最近被问得最多的一个词就是 Agent,也就是智能体。很多刚接触 AI 的朋友一看到“Agent 实战”“多智能体协作”这类标题,第一反应是觉得门槛很高,得先会用各种框架,得懂一大堆底层原理。其实我自己的经验正好相反:从一个最简单的 Agent 开始,只靠 Python 和一个大模型 API,半天时间就能跑通完整的“让 AI 自己干活”的流程。这篇内容我按照自己从概念到实战构建的路径来写,尽量不堆术语,每一步都告诉你为什么要这么写、踩过哪些坑,适合完全零基础的小白,也适合想快速把手头的大模型 API 用起来的开发者。建议先收藏,因为后面代码部分可以直接当模板抄。
1. Agent 到底是什么?别被新概念唬住
1.1 用一句话先给 Agent 下个定义
我理解的 Agent,可以拆成一个公式:Agent = 大模型 + 记忆 + 工具 + 行动循环。如果拿普通聊天机器人和它对比,聊天机器人像是一个只能坐在办公室里回答问题的笔友,你问一句它答一句,它不会主动去查资料、不会调系统、也不会事后帮你跟进。而 Agent 更像一个带着手机、记事本和公司内部系统的外勤助理,你交代它“把明天的会议安排整理成邮件发给我”,它能自己拆步骤、查日历、调用邮件接口、最后把成品交到你手上。
这里的关键点是:Agent 本身并没有“自主意识”,它的聪明来自大模型的推理能力,而它的“动手能力”来自程序给它外挂的工具。你搭建 Agent,本质上就是在写一段流程代码,让大模型在一个循环里不断做决定:下一步该调用哪个工具、还是直接给出答案。把这个概念理解透了,后面的实战就不会觉得玄乎。
1.2 Agent 的核心环节:感知、决策、行动、反馈
我在自己做得比较顺手的演示项目 X 里,把 Agent 的工作方式归纳成四个环节:
- 感知:接收用户输入,以及从外部读取必要信息。比如用户说“整理一下我今天的日报”,Agent 要先把这句话变成结构化任务。
- 决策:大模型根据任务和目标,决定下一步动作。是直接写回答,还是先查询某个工具的数据。
- 行动:执行代码里的工具函数,比如读取待办列表、请求天气接口、操作文件。
- 反馈:把工具执行后的结果返回给大模型,让模型基于真实数据生成最终回复。
这四个环节形成一个闭环。没有这个闭环,大模型就只能“想”不能“做”;加上这个闭环,它才像在干活而不是在聊天。
1.3 它和大模型的直接调用有什么不同
过去我们调大模型接口,通常是一段 prompt 进去,一段文本出来。输出好不好,只取决于提示词写得怎么样。而 Agent 的差异在于:模型可以输出一种“工具调用指令”,而不是直接输出最终答案。代码收到这个指令后,去执行真实的函数,再把结果塞回对话里,让模型继续推理。
我经常打一个比方:普通 API 调用是“只让一个很聪明的人闭卷答题”,Agent 是“让这个人可以翻书、查资料、拿计算器,甚至打电话问同事”,最后交出来的答案准确性自然高很多。所以零基础入门时,先把注意力放在“工具调用 + 循环”这两个机制上,比你背一百个框架 API 更有用。
2. 零基础入门的准备:工具、成本与思维模型
2.1 你需要准备的技术栈其实很少
市面上讲 Agent 的教程经常会推荐各种编排框架,比如某些“链”后缀的库。我不是说它们不好,而是对于零基础,先把框架放一边,手写一个最小闭环才是最快的路径。你真正需要准备的只有这些:
- Python 3.10 以上环境,能跑脚本就行。
requests库,用来发 HTTP 请求。安装命令很简单:pip install requests。- 一个大模型 API 的访问密钥。现在这类服务已经很普遍,选一家你能注册的、支持“工具调用”或“函数调用”能力的服务即可。
- 一个文本编辑器,我习惯用 VS Code,但你用什么都行。
不需要 GPU,不需要 Linux 服务器,不需要 Docker。你的电脑只要能运行 Python,就能跑通下面这个例子。我第一次跑通的时候,甚至是在一台只有 8GB 内存的老笔记本上完成的。
2.2 调用大模型 API 的成本到底贵不贵
很多小白会担心费用。我的经验是:做学习和演示用的 Agent,成本几乎可以忽略,但要注意控制循环次数。当前大部分大模型 API 按 token 计费,输入和输出价格不同。我以一个虚拟价格为例:输入每百万 token 10 元,输出每百万 token 30 元。一次普通的日报整理请求,可能消耗几百个 token,算下来单次成本不到一分钱。
真正烧钱的是两件事:
- 没有限制的 while 循环,让 Agent 在一个错误分支上反复调用几十次甚至上百次。
- 把超长文档反复塞进上下文,每次都全量发送,token 费用线性增长。
所以我会在代码里给循环加一个最大步数,比如max_steps=5,既能防止失控,也方便排查问题。这个习惯不管做学习项目还是生产项目都建议保留。
2.3 核心思维:ReAct 循环
ReAct 是 Reason + Act 的组合,翻译过来就是“推理 + 行动”。这个思路在 Agent 领域非常重要,但理解起来不难。设想用户问“明天出门要带伞吗?”,一个普通模型可能直接回答“建议带伞”,但它是凭训练时的记忆猜的。而一个带 ReAct 的 Agent 会这样做:
- 思考:这个问题需要知道明天的天气,所以我应该调用天气查询工具。
- 行动:调用
get_weather工具,传入城市名和日期。 - 观察:工具返回“明天多云,午后有阵雨”。
- 思考:既然有阵雨,我应该建议用户带伞。
- 行动:给出最终答案。
在代码层面,这五个步骤就是在循环里反复调用大模型接口,判断返回内容里是否有“工具调用”字段。有就执行工具;没有,就把当前内容作为最终输出返回。这个机制,就是 Agent 的发动机。
3. 实战构建:一个日报整理 Agent 从 0 到 1
3.1 先把需求拆解成工作流
我选的例子是“日报整理 Agent”。为什么选它?因为日报每个人都熟悉,需求边界清晰,而且它能自然地用到两个常用工具:读取待办列表、获取当日天气。用户输入一段话:“我上午写了方案文档,下午开了两个会,明天可能要外出拜访客户”,Agent 需要输出一份这样的日报:
- 今日完成事项
- 明日计划
- 明日天气提醒
- 总结/建议
如果只靠大模型直接生成,它很容易给你编一个“明天晴”之类的假数据。所以我们要让它先调用天气工具,拿到真实天气后再生成日报。工作流可以文字表述为:接收用户输入 → 把工具列表和对话历史一起发给模型 → 模型判断需要调用天气/待办工具 → 代码执行对应函数 → 把结果返回给模型 → 模型生成最终日报。这样一遍流程走下来,就已经是一个完整的单 Agent 应用了。
3.2 第一次调用大模型 API
先写最基础的调用函数。假设你已经在环境变量里配置了LLM_API_KEY。下面这段代码,我用的端点是一个通用占位地址,实际使用时要替换成你所用服务的真实地址。
import os import requests import json API_KEY = os.getenv("LLM_API_KEY") API_URL = "https://api.example.com/v1/chat/completions" def call_llm(messages, tools=None): headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } payload = { "model": "your-model-name", "messages": messages, "tools": tools or [], "tool_choice": "auto", } resp = requests.post(API_URL, headers=headers, json=payload, timeout=30) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]这段代码的要点:
messages是完整的对话历史,不只是当前用户消息。因为 API 本身无状态,我们必须自己携带上下文。tools是工具描述列表,告诉模型有哪些函数可以调用。tool_choice设为auto,让模型自己判断什么时候调用工具、什么时候直接回答。
第一次跑通了,别急着往下写,先打印一下返回的 message 内容,你会看到两种典型结果:一种是正常的content字段,直接包含回答;另一种是模型返回了一个tool_calls字段,里面是它想调用的函数名和参数。看到后者,说明模型已经具备 Agent 的“决策”能力了。
3.3 给 Agent 配上两个简单工具
工具本质就是普通的 Python 函数,但我们需要把它们的“说明书”告诉模型。先写函数本身:
def get_weather(city: str, date: str) -> str: # 演示用假数据,实际应请求天气服务 if "雨" in "明天有阵雨": return json.dumps({"city": city, "date": date, "weather": "多云转阵雨", "temperature": "22-28°C"}, ensure_ascii=False) return json.dumps({"city": city, "date": date, "weather": "晴", "temperature": "20-30°C"}, ensure_ascii=False) def read_todos(date: str) -> str: todos = { "2025-01-06": ["上午写方案文档", "下午参加产品评审会", "晚上准备客户拜访材料"], "2025-01-07": ["上午十点拜访客户", "下午整理合同"], } items = todos.get(date, []) return json.dumps({"date": date, "todos": items}, ensure_ascii=False)两个函数都返回 JSON 字符串,这么做有个好处:结构清晰,大模型能稳定解析。接下来是工具描述,要告诉模型“函数叫什么、有什么用、参数是什么格式”。这部分大多数 API 都遵循一套通用的 JSON Schema 描述方式。
tools = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市在指定日期的天气情况,返回天气和温度", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名"}, "date": {"type": "string", "description": "日期,格式 YYYY-MM-DD"} }, "required": ["city", "date"] } } }, { "type": "function", "function": { "name": "read_todos", "description": "读取指定日期的待办事项列表", "parameters": { "type": "object", "properties": { "date": {"type": "string", "description": "日期,格式 YYYY-MM-DD"} }, "required": ["date"] } } } ]这里有一个非常重要的点:description一定要写清楚“什么时候用”。模型没有读函数内部代码的能力,它只能靠这段描述来判断该调用谁。你描述得越具体,模型的选型就越准。我见过很多初学者在这里随便写一两句话,导致模型经常选错工具或者干脆不调用。
3.4 让 Agent 在循环里自己决定下一步
现在把上面几块拼起来,实现一个最核心的循环。模型第一次返回时,如果没有tool_calls,说明任务完成;如果有,我们就执行对应的工具,把工具结果以新的消息追加到messages里,然后带着更新后的历史再次调用模型。如此反复。
def run_agent(user_input, max_steps=5): messages = [ {"role": "system", "content": "你是一个日报整理助手。你会根据用户描述和工具返回的真实数据生成日报,不要编造工具结果。"}, {"role": "user", "content": user_input}, ] for step in range(max_steps): message = call_llm(messages, tools=tools) # 如果没有工具调用,说明这是最终回答 if not message.get("tool_calls"): return message.get("content", "") messages.append(message) for tool_call in message["tool_calls"]: func_name = tool_call["function"]["name"] arguments = json.loads(tool_call["function"]["arguments"]) if func_name == "get_weather": result = get_weather(city=arguments["city"], date=arguments["date"]) elif func_name == "read_todos": result = read_todos(date=arguments["date"]) else: result = json.dumps({"error": "unknown tool"}) messages.append({ "role": "tool", "tool_call_id": tool_call["id"], "content": result, }) return "达到最大步骤数,任务未完成。"测试时可以用这样的输入:
if __name__ == "__main__": user_input = "今天是2025-01-06,我写了方案文档,下午开了两个会,明天上午要拜访客户。请帮我生成今日日报,并提醒我明天北京的天气。" print(run_agent(user_input))这里有一个代码细节值得留意:在messages.append(message)时,我直接把模型返回的那一整条消息放回去了。那条消息里包含content和tool_calls。如果不把tool_calls放进去,模型就不知道“我刚才提出了调用请求”,可能陷入前后矛盾的状态。
运行后会看到什么?模型会先返回一个read_todos调用,代码执行后拿到待办列表;接着可能再返回一个get_weather调用,拿到天气;最后生成一段结构化日报。每一步打印出来,你就能直观地感受到 Agent 的工作方式了。
4. 细节决定成败:提示词、记忆与上下文管理
4.1 提示词不玄学,但要讲结构
很多人在 Agent 上失败,第一原因不是代码错了,而是提示词太模糊。系统提示词是模型的全局约束,我一般会包含四部分:角色定位、任务目标、可用工具、输出约束。拿日报 Agent 来说:
你是一个日报整理助手。 根据用户提供的当日工作描述和工具返回的真实数据,生成一份中文日报。 你有以下工具: - read_todos(date):读取指定日期的待办事项,结果作为今日完成事项的参考。 - get_weather(city, date):查询城市天气,结果用于明日计划和出行提醒。 你必须基于工具返回值来写日报,不得编造天气和待办数据。 日报格式如下: ## 今日完成 ## 明日计划 ## 天气提醒 ## 建议这里最关键的一句是“必须基于工具返回值来写日报,不得编造”。不加这句,模型会倾向于图省事,直接在第一次回答时伪造一个天气结果。原因很简单:模型的任务是补全对话,它并不知道自己真的可以调用工具,约束越明确,行为越稳定。
4.2 在无状态 API 下实现记忆
大模型 API 本身不保存会话记录,所以“记忆”完全靠我们维护 messages 列表。短记忆就是把每一步的 user、assistant、tool 消息都追加到列表里,这个我们在 3.4 已经做了。长记忆则要更主动地保存关键信息,比如用户常用城市、项目背景、历史偏好。入门阶段不需要上向量数据库,用 JSON 文件就够:
import json memory_file = "agent_memory.json" def save_memory(key, value): with open(memory_file, "r", encoding="utf-8") as f: memory = json.load(f) memory[key] = value with open(memory_file, "w", encoding="utf-8") as f: json.dump(memory, f, ensure_ascii=False, indent=2)然后在系统提示词里把记忆内容拼接进去,例如“用户常驻城市:北京”。这样 Agent 在查询天气时,如果用户没给城市,它也能从记忆中推断。这个设计虽然简单,但能让你体会到“有状态 Agent”和“无状态模型”之间的桥梁是怎么搭起来的。
4.3 上下文窗口不够用怎么办
上下文窗口是每个 Agent 开发者都会撞上的墙。尤其是对话轮数增多、工具返回结果很大时,messages 列表会越来越长,费用和延迟都会上来。我常用的办法有三个:
- 截断:只保留最近 N 条消息,最早的消息丢进一个摘要变量里。
- 摘要:每隔几轮,让模型把前面的对话压缩成一段摘要,作为 system 消息重新放回去。
- 工具结果瘦身:如果工具返回几十条数据,可以模型先筛选,只把关键字段传回 agent。比如天气接口返回十几项指标,日报只需要天气和温度,那工具函数里只保留这两项。
一个简单的摘要函数可以这样写:
def summarize_messages(messages): summary_prompt = "请用三句话概括以上对话的关键信息,包括用户偏好和未完成事项:" summary_messages = messages + [{"role": "user", "content": summary_prompt}] summary = call_llm(summary_messages)["content"] return [{"role": "system", "content": f"历史摘要:{summary}"}]注意,这个函数本身又是一次 API 调用,所以在小 demo 里不要频繁触发。我的经验是:当 messages 的总字符数超过约 8000 字,或者连续 6 轮以上还没结束,再做摘要比较划算。
5. 踩坑实录:Agent 实战中的常见问题与排查技巧
5.1 模型“编造”工具结果,你没有及时发现
这是最典型的坑。表现为模型返回了一段“天气:晴,气温 25°C”,但你根本没有在 messages 里看到tool_calls,或者工具函数根本没有被执行。原因通常是系统提示词不够强、模型不支持工具调用、或者你用了不支持 function calling 的模型。
排查方法很简单:在run_agent的循环里加一段打印,把每次调用的message原样输出到控制台。如果发现“直接给出天气”但没有任何工具调用记录,就是模型在编造。解决方式是:
- 在提示词中加入“你无法直接获取天气,必须调用 get_weather 工具。”
- 确认所用模型支持工具调用,不要用只适合聊天的基础版本。
- 在最终输出校验时,检查日报里的天气是否来自工具返回字段,如果发现日期对不上,重新发起一次修复请求。
5.2 循环卡死与预算失控
Agent 循环如果写得不够稳,很容易出现“模型反复调用同一个工具,拿到的结果每次都一样,但它还在继续调”的情况。这会直接烧光预算。我在排错时遇到过某次循环连续调用了 20 次天气接口,原因就是提示词让它必须确认三次才能输出,但模型每次返回的参数有细微差异,导致逻辑反复触发。
解决办法有三层:
- 设置
max_steps,我习惯默认 3-5 步。 - 给每次请求加
timeout=30,防止网络阻塞挂死。 - 对同样的工具调用计次,如果同一个
tool_call_id或函数名+参数连续出现超过 2 次,直接强制跳出,并让模型基于已有结果生成答复。
第三层属于兜底逻辑,虽然会打断模型的节奏,但至少不会让预算失控。生产环境里,我还会额外加上一个日志计数器,超过阈值就发告警。
5.3 输出格式不稳定,解析经常报错
模型偶尔会把 JSON 输出成带```json的代码块,或者在里面写注释,导致json.loads直接失败。遇到这种情况,不要急着骂模型。写一个健壮的解析函数,它先把字符串里的 markdown 代码块剥离,再尝试解析 JSON。
import re def parse_tool_arguments(text): text = text.strip() # 去掉 ```json ... ``` 包裹 match = re.search(r"```(?:json)?\s*([\s\S]*?)```", text) if match: text = match.group(1).strip() return json.loads(text)如果解析失败,不要直接给用户报错。把解析的错误信息作为 user 消息传回模型,比如“你上一步返回的 JSON 无法解析,请重新输出有效的 JSON”。模型通常会自动修正。这个“错误回灌”技巧,比人工介入快得多。
5.4 我的排查顺序与避坑清单
做 Agent 调试时,我有一套固定的排查顺序,能省下大量时间:
- 先看 HTTP 请求是否成功,Authorization 有没有拼对。
- 打印模型第一次返回的原始 message,确定到底有没有
tool_calls。 - 确认 tool_calls 里的函数名和参数是否准确,参数类型是否都能转成 Python 对象。
- 确认工具函数有没有真的执行,结果有没有以
role: "tool"追加回去,tool_call_id是否和请求的一致。 - 最后才怀疑提示词,不要一上来就改提示词,那样容易越改越乱。
关于避坑,再分享几条日常实战总结:任何工具函数都要给默认值,比如城市默认“北京”,日期默认“今天”,减少模型遗漏参数的概率;工具返回结果统一用 JSON 字符串,字段名保持稳定;不要把真实 API 密钥写在代码里,用环境变量;每次改动提示词后,先跑同一个用例做回归对比,不然很难判断是代码问题还是模型波动。
我个人折腾下来最大的体会是:Agent 的门槛没有想象中高,但坑一定比教程里写得多。你不需要第一天就把所有框架都学会,只需要先手动把“模型发出工具调用 → 代码执行 → 结果回传 → 再次调用模型”这条闭环跑通,就已经很了不起了。后面遇到多 Agent 协作、复杂编排、知识库接入,本质上都是在“让我手写的这个最小循环”更可控、更强大。另外还有一个小技巧:把你这套 Agent 的输入输出和每次工具调用日志保存下来,哪怕只是存成本地文件,调试时都是一种巨大的优势。希望这篇内容能成为你入门 Agent 的第一块垫脚石,照着上面的例子动手做一遍,比看二十篇教程都管用。