聊点AI Agent的实操干货。这个系列写到第三篇,前两篇把Agent的基础认知和规划能力过了一遍,这篇专注落地——怎么把一个Agent真正跑起来。很多人学Agent卡得最厉害的地方不在概念层,而在实操层:架构图看懂了,Plan、Act、Observe、Reflect这些环节名字也熟了,但打开编辑器准备写代码时,配工具、调参数、处理流程上的各种意外,就完全找不到抓手。这篇文章用一个具体的任务场景,从整体设计到代码落地,再到参数调优和问题排查完整走一遍,全文包含可直接借用的代码片段和一张简单的流程示意图。有一定Python基础、想跳过纯理论直接上手Agent的朋友,按这篇的节奏走就行。
1. 内容整体设计与思路拆解
1.1 先搞清楚:大模型只是“大脑”,真正干活的是一套循环
不少刚接触Agent的朋友有一个误解:觉得Agent就是“把问题丢给大模型,模型自己就干完活了”。真要这么简单,市面上就不需要那么多Agent框架了。大模型本身的能力边界是生成文本,它只有token输入输出的能力,既不能真的去查数据库,也不会去调外部接口,更没法自己拿起鼠标操作电脑。Agent的本质,是搭在模型外面的一层“进程控制逻辑”——让模型在一个循环里反复“思考、决策、调用工具、观察结果、再思考”,直到完成任务或到达退出条件。
打个比方:你直接问大模型“帮我查一下这几个城市的天气和航班价格”,它只能给你一个模糊回答,或者告诉你“我没法实时查询”。但你给一个Agent同样的指令,它会自己拆解任务:先调用天气接口查天气,再调用航班查询接口查价格,把结果整理成一张表返回给你。这中间多出来的部分,不是模型的智力,而是循环里的控制逻辑。
所以理解Agent的分水岭,不是模型选得多强,而是你是否把“规划—执行—观察—再规划”这个循环写对了。这也是这个系列里我认为最核心的主题,因为这个循环一旦跑通,后面加工具、加记忆、加多Agent协作,都只是往骨架上添东西。
1.2 Agent的典型运行循环长什么样
在写代码之前,先把循环画清楚。一个最标准的Agent循环通常是这个顺序:用户输入,模型生成“下一步计划或工具调用”,执行工具调用,把工具返回结果回填给模型,模型根据新信息决定继续调用还是给出最终答案,输出最终答案。有一个简化版的流程可以放在脑子里,我画成文本形式就是下面这样:
用户指令 ↓ Agent接收并解析 ↓ 大模型推理:判断需要哪些信息 ↓ 调用工具A(如查天气API) ↓ 拿到结果并回传模型 ↓ 模型判断:信息是否足够? ├─ 不足 → 继续调用下一个工具 └─ 足够 → 生成最终回复并返回用户这个循环在相关的技术课程和开源框架里经常被叫Agentic Loop、ReAct、Plan-and-Execute等,名字不同,核心思路都是一个。网上也有很多经典的论文和课程在讲这些范式,对方向可以不用纠结术语,抓住“循环”这两个字就够了。
我刻意不在这篇里选择“多Agent协作”“复杂工作流”这类炫技方向,而是选最朴素、最通用的单Agent循环做演示。原因很简单:大多数真实场景,比如数据聚合、定时任务、报表整理、信息检索,本质上都能拆成这个循环。把最基础的一条Loop吃透,比急着上复杂架构要实用得多。
1.3 演示场景:让Agent自动汇总多源信息
这篇里选的演示任务是:让Agent根据用户一句话,去多个数据源查询信息,并汇总成结构化结果。具体来说,让它做“查询指定城市的天气,并判断是否适合出门”。这个任务看起来小,但涵盖了Agent几乎所有的核心要素:需要拆解任务、需要调用外部工具(天气查询函数和时间判断函数)、需要把结果回传给模型做二次推理、还需要在多次调用之间保持正确的上下文。
这类“查询聚合型”任务放在教学场景里有几个独特优势。第一,它对工具的定义非常清晰,不涉及文件系统、浏览器交互这些容易出问题的环节。第二,它的目标是唯一的,方便判断成功失败,适合验证循环代码是否写对。第三,它很容易扩展:今天查天气,明天换成查库存、查订单、查日志,循环代码一行都不用改,只需要换工具函数。这就是为什么很多人学Agent的第一个Demo都从这类任务开始。
2. 核心细节解析与实操要点
2.1 规划模块:别迷信“先出完整计划”
早期的Agent设计喜欢让模型一开始就输出一个完整执行计划,然后按计划执行到底。后来大家发现这种方式相当脆弱:真实任务里经常会在执行到一半时发现初始信息不够,或者某个工具返回的结果和预想不同,这时候死板地按原计划走就会卡死。所以现在主流的做法是边做边规划,每走一步都让模型根据当前状态重新决策:下一步该调哪个工具,还是直接给出回答。
对应到代码上,规划模块不需要单独写成一个函数,它就是循环里的一个步骤。我在代码里也没有单独建一个“planner”对象,只是每次都把最新的对话历史重新发给模型,让它决定下一步动作。这种设计有几个好处:实现简单、状态天然保存在消息列表里、出错后好调试。缺点也有:每次循环都要完整发一次历史消息,token成本会偏高,但对教学Demo来说,清晰比省钱更重要。
有人可能会问:那大模型的上下文不是有限吗?历史消息越堆越长怎么办?这个问题放在2.3节的记忆管理里一起说,这里先留个引子。
2.2 工具注册与调用:每个工具就是一张函数签名
工具调用模式,通常叫Tools或Function Calling,是Agent区别于纯聊天的关键。说白了,就是定义一批真实能执行的Python函数,把它们“描述”给模型,让模型在认为需要的时候发出请求来调用它们。为了让模型知道有哪些工具可用、分别在什么场景下用,我们需要把每个工具的名字、参数结构、用途说明写得清清楚楚,这些描述会作为附加信息拼进模型的请求里。
实操上,我习惯用一个字典来维护工具集合:
def get_weather(city: str) -> str: """查询城市实时天气,返回温度、天气状况。""" # 这里应该请求真实天气API,Demo里直接返回模拟数据 return f"{city}:22℃,晴" def judge_suitable(weather_desc: str) -> str: """根据天气描述判断是否适合出门。""" if "雨" in weather_desc: return "有雨,不建议长时间户外活动" return "天气不错,适合出门" TOOLS = { "get_weather": { "func": get_weather, "desc": "查询指定城市实时天气,参数为城市名称" }, "judge_suitable": { "func": judge_suitable, "desc": "根据天气情况判断是否适合出门,参数为天气描述" }, }这里把“工具描述”简化成了字符串,实际接大模型API时,需要把每个工具的参数格式写成JSON Schema,比如get_weather需要一个字符串参数city。模型看到这个Schema之后,就会在合适的时机主动生成类似“调用get_weather,参数city为北京”的请求。这一整块逻辑,从实践来看,最好在函数定义阶段就给足信息,后面调试会省很多事。
2.3 记忆管理:别让历史消息无限膨胀
记忆模块是Agent容易忽视但非常影响稳定性的部分。前面说到的循环里,每一轮我们都把新增的工具返回结果追加到消息列表里继续发给模型。如果不加控制,执行到第10步、第20步时,消息列表可能已经塞满了工具返回的长文本,不仅增加token成本,还可能挤掉关键指令,导致模型“忘记”原始目标。
我见过一些新手写的Agent跑着跑着就行为失控,排查了半天发现是历史消息里某个工具返回了几千字的日志,最早的原始用户指令被挤到了上下文深处,模型其实已经看不到最开始的任务了。记忆管理最朴素的手段有两种:一是超过一定轮数后把早期的工具返回做摘要替换,二是每次只保留最近N轮的完整消息,更早的浓缩成一句摘要。对于教学场景,我建议先做第二种:对话历史超过比如10轮之后,把最早的用户指令放到系统提示词里固定保留,中间的过程全部压缩成“已完成步骤摘要”。不要等到出现明显症状才开始管记忆,记忆管理应该在写循环的第一天就设计进去。
2.4 反思与自我修正:给循环装一个“刹车”
反思模块,理解成人话就是让Agent在输出最终结论前,先自己检查一遍结果是否合理。很多Demo里会漏掉这一步,结果就是模型给出一个看起来像样、实际错误百出的答案。我给循环里加了一个轻量级的反思步骤:在模型准备输出最终答案时,强制它把已获取的信息和原始任务目标对照一眼,哪里缺了就补哪里。
这一步不一定每个项目都必需,但对准确率要求高、或者工具调用链路较长的任务,它能明显降低失败率。加上反思的成本也很低,无非是多一次模型调用,在一个5步以内的循环里增加的那点延迟完全可以接受。如果你是抱着“先跑通Demo”的心态,那可以先把基本循环跑通,反思后面再补也不迟。
3. 实操过程与核心环节实现
3.1 环境准备:一个干净的运行环境
先把运行环境准备好。我建议用虚拟环境来装Python依赖,避免把宿主机目录弄乱。本轮Demo需要的Python库并不多,核心只有两类:一类是调用大模型API的客户端库,另一类是用于读取环境变量和管理密钥的库。工具函数里如果需要发HTTP请求,按需再装HTTP库。
我建议在项目根目录建一个.env文件保存API密钥,用配置管理相关库来读取,不要把密钥直接写死在代码里。这不仅是安全问题,也是工程习惯问题。别人拿到你的代码,看不见密钥也能跑通一部分流程;你将来换模型、换密钥时,也不用到处翻代码替换。环境准备好之后,把大模型API的基础连通性测一遍,确认能正常拿到回复,再继续下一步。
3.2 核心代码:一个极简Agent循环
下面这段代码是整篇的核心,我把它精简到了能说明问题的最低限度。它没有依赖任何Agent框架,只用了一个普通Python函数加一个循环:
def call_llm(messages, tools_schema): """调用大模型接口。 这里是演示用,真实使用时替换为你对接的API逻辑。 返回值约定:{"content": 模型文本, "tool_calls": [工具调用列表]} """ if any(msg.get("role") == "tool" for msg in messages): return {"content": "根据查询结果,北京今天22℃晴,适合出行。", "tool_calls": []} return { "content": None, "tool_calls": [ {"name": "get_weather", "arguments": {"city": "北京"}} ] } def get_weather(city: str) -> str: """查询城市实时天气,Demo中直接返回固定值。""" return f"{city}:22℃,晴" def judge_suitable(weather_desc: str) -> str: """根据天气情况判断是否适合出门。""" if "雨" in weather_desc: return "有雨,不建议长时间户外活动" return "天气不错,适合出门" TOOLS = { "get_weather": { "func": get_weather, "schema": { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的实时天气", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"} }, "required": ["city"] } } } }, "judge_suitable": { "func": judge_suitable, "schema": { "type": "function", "function": { "name": "judge_suitable", "description": "根据天气描述判断是否适合出门", "parameters": { "type": "object", "properties": { "weather_desc": {"type": "string", "description": "天气描述文本"} }, "required": ["weather_desc"] } } } } } def run_agent(user_input: str, max_steps: int = 5) -> str: messages = [{"role": "user", "content": user_input}] for step in range(max_steps): resp = call_llm(messages, [t["schema"] for t in TOOLS.values()]) if resp.get("tool_calls"): for call in resp["tool_calls"]: tool = TOOLS.get(call["name"]) if not tool: messages.append({ "role": "tool", "content": f"错误:工具{call['name']}不存在" }) continue result = tool["func"](**call["arguments"]) messages.append({ "role": "tool", "content": str(result) }) continue if resp.get("content"): return resp["content"] return "模型没有返回有效内容,已停止" return "达到最大步数,任务未完成" if __name__ == "__main__": result = run_agent("北京今天天气怎么样?适合出门吗?") print(result)这段代码里,call_llm函数给的是一个演示用的假实现,它会在对话历史中没有工具返回结果时,假装请求调用get_weather工具;一旦发现有工具结果了,就假装模型给出了最终答案。替换成真实API时,只需要在这个函数里把messages和tools_schema发出去,再解析返回结果,把返回结构保持为“content + tool_calls”就行。我把这处替换单独拎出来说,是因为很多人第一次写Agent就卡在“我不确定API返回格式怎么适配”上,本质上只要把API返回格式映射到自己的内部结构,其余逻辑就能完全复用。
3.3 参数选择与调试:哪些参数对Agent最关键
跑通之后,调试Agent主要看四个参数。
第一是max_steps,也就是循环的最大步数。设得太小,任务稍微绕一点就提前结束,输出一个“未完成”;设得太大,遇到模型反复调用同一个工具时就会空转浪费token。经验值:单工具类任务设3到5,多工具串联任务设8到10,具体什么时候该加、什么时候该减,以任务拆解的最坏情况为准。
第二个是温度参数。很多同学直接用默认值,但在Agent场景里我强烈建议调低,通常0到0.3之间,我常用0.2。原因很直接:工具名称、参数名、JSON格式这些内容都是确定性的,温度越高模型越容易在生成时“即兴发挥”,产生幻觉函数名或非法JSON。写代码时,我们希望模型严格按照工具规范去调用,而不是给它创作空间。
第三是模型的上下文长度。这个参数看起来是模型侧的,但直接决定Agent能跑多复杂。不同模型的上下文窗口差异很大,如果你的Agent每个循环都要带全量历史,长窗口支持的是更复杂的多轮工具链,短窗口就必须用前面说的摘要压缩方案。动手做之前,先查清楚自己用的API能传多长的输入,再决定设计策略,可以省掉很多后期重构。
第四是工具返回内容的长度控制。这个很多人容易忽略。你调了一个查询接口,返回一份10万字的报告,直接拼进对话历史,模型处理起来又慢又容易丢重点。我习惯在工具函数里做一层“预处理再回传”:把结果压缩成摘要、只截取关键字段、控制每次工具返回不超过几百字。传给模型的信息是让它做决策的,不是做全文阅读的,垃圾进就没法判断。
3.4 流程控制与终止条件:保证Agent不会无限循环
无限循环是Agent开发里最典型的故障。模型拿到一个工具的结果不满意,重新调用同一个工具,结果还是不满意,再调用……如果循环没有终止条件,这一串调用会一直持续到预算烧光。所以终止条件必须有,而且最好是多重保险:一是max_steps兜底,二是工具返回结果里如果连续多次相同,主动中断并让模型重新规划,三是设置单轮操作超时时间,防止某个外部接口把整个流程卡死。
对新手我特别推荐一个方案:在循环里增加“连续相同调用计数”,当模型连续三次请求同一个工具且参数完全一致时,直接把对应消息处理成一句提示:“工具已连续返回相同结果,请停止重复调用,改用其他方式完成任务。”这个提示在实测中对抑制循环空转很有效,代码不过几行,但稳定性提升非常明显。
4. 常见问题与排查技巧实录
4.1 常见问题排查表
下面这个表格,我按“症状—原因—解法”的方式整理了实操中最常遇到的几类问题,建议存一份,后面写Agent出问题时可以先对着它排查。
| 症状 | 可能原因 | 解决思路 |
|---|---|---|
| Agent反复调用同一个工具 | 模型在循环里没有形成新的决策依据,工具返回信息没被参考 | 检查工具返回是否被正确追加到消息历史;增加“连续相同调用”中断逻辑 |
| 输出里出现不存在的函数名 | 工具描述不清晰,或温度参数过高 | 检查工具Schema里的name和description,调低温度到0.2附近 |
| 模型返回的JSON解析失败 | 模型输出了多余文字,格式不干净 | 在解析前先截取JSON块;用强格式约束提示词;增加解析容错 |
| 任务还没完成就提前结束 | max_steps太小,或模型误判任务完成 | 调大max_steps;在系统提示词里强调“没有拿到最终信息前不要结束” |
| 上下文越长效果越差 | 历史消息里堆了大量工具返回的原始结果 | 做消息摘要;限制工具返回长度;只保留最近N轮完整消息 |
| 工具真实执行报错,但Agent直接把这个错误当结果 | 工具异常没有做包装处理 | 在工具调用侧统一捕获异常,把错误信息明确包装成“调用失败,请重试或换工具”返回给模型 |
4.2 两个我踩过的坑:日志和异常处理
第一个坑:没有给Agent留完整的运行日志。循环逻辑一旦跑起来,中间过程非常多:模型每次生成了什么、工具返回了什么、什么时候决定终止,这些信息如果没有记录,出了问题只能对着最终结果靠猜。我在早期调试时吃过这个亏,一个循环看起来明明通了,但结果偶尔不对。最后加了一行一行打印每一步的messages,才定位到问题出在“一次工具调用返回了一个错误报文,模型把这个报文当成正确答案继续往下走”。从那以后,我写Agent必带详细的调试日志开关,平时关闭,排查问题时打开。
第二个坑:工具异常被静默吞掉。正常情况下,一个工具函数可能在调用外部接口时抛异常,如果异常被某个层的try-except悄悄吞掉,返回一个“None”或空字符串,模型很容易理解成“查询成功,没有数据”,然后一本正经地给出错误结论。比较好的做法是:异常不要吞,包装成一条错误消息返回给模型,让它知道刚才的调用失败了,可以换个参数重试或选择其他路径。
4.3 关于工具描述的几个细节
工具描述在新手阶段容易被忽略,但影响很大。我踩过的一个具体例子:早期我给一个查询函数写了很长的、包含各种边界情况的描述,结果模型反而不知道该什么时候调用它,任务里明明很适合,却一直找别的工具替代。后来把描述精简成一句“当用户想查询某城市天气时调用此函数”,效果立刻变好。描述要短、要聚焦触发场景,而不是把实现细节写进去。
另外,工具参数名也很讲究。参数名最好明确无歧义,比如city、start_date,不要用a、b这类缩写。模型能接收到的有效信息量是有限的,参数名和描述,要把信息用在刀刃上。
5. 工具选型与进阶方向
5.1 什么时候自己写循环,什么时候用框架
自己写循环的好处是透明、可控、没有黑魔法,适合学习阶段和轻量场景。上面那个几十行的代码已经能处理一类任务了,如果只是要跑一个内部小工具,真的不需要引入重型框架。反过来,如果项目要对接很多外部工具、要管理复杂的记忆和权限、要支持多用户并发,再自己从零写,成本就上去了。这时候用主流的Agent框架,好处是很多细节,比如消息管理、工具注册、重试机制、可观测性,已经处理好了,你只需要聚焦业务逻辑。
我自己的选择标准是:任务闭环能在几十行以内写完,就自己写;如果代码量预计超过几百行,或者预计要长期维护,就上框架。不要一上来就纠结用哪个框架,框架只是个壳,核心还是你懂不懂Agent循环本身。把上面那个Demo写熟之后,再去看框架文档,会发现很多概念都可以对应上。
5.2 从Demo到生产环境还差什么
一个能跑通的Demo,距离一个能在生产环境稳定运行的Agent,还有几个关键差距。
第一是稳定性和容错。Demo可以假设外部接口永远正常、模型格式永远符合预期,生产环境不可能这么乐观。前面说的日志、超时、重试、异常包装,在Demo里可以没有,生产环境一个不能少。
第二是可观测性。生产环境的Agent,你要能追踪它每一步做了什么、为什么这么做、花掉了多少成本。建议把每一步的关键信息,包括模型调用、工具调用、耗时、Token数,记录到结构化日志里,将来排查问题会轻松非常多。
第三是权限与安全。生产环境的Agent能调用什么工具、访问什么数据、执行什么操作,都必须有明确边界。不要让Agent在无人监管的情况下直接执行高风险操作,尤其是涉及删除、支付、发送消息这类动作,至少要加一个人工审批环节。
第四是评估。你有没有一套测试用例来持续验证Agent的表现?改了一次提示词,行为有没有变差?换了一个工具参数,成功率掉了吗?这些没有量化手段是答不上来的。我在迭代Agent时,会固定一批包含正常情况和边界情况的测试用例,跑一遍记录成功率,再决定改还是不改。这套方法不太好自动化,但人肉做也比纯凭感觉强。
5.3 进阶方向:更多工具、持久记忆、多Agent协作
这篇文章只演示了单Agent单循环。再往下走,有几个自然的进阶方向。
第一个方向是让Agent能使用更多类型的工具,比如文件读写、数据库查询、网页访问、代码执行。工具类型的丰富程度,直接决定了Agent能处理任务的广度。
第二个方向是给Agent加上持久化记忆,让它能在多次会话之间记得用户偏好和历史状态。这跟单次循环里的上下文管理不是一回事,它需要考虑存储、检索和更新,复杂度明显上升。
第三个方向是多Agent协作:把一个复杂大任务拆成几个角色分工协作,比如一个规划Agent、一个执行Agent、一个审查Agent。多Agent不是银弹,它解决的是“单Agent上下文过载”的问题,但会带来更复杂的协调成本,有时候也会出现错误在各层传递放大的情况。个人建议:先把单Agent玩透,再考虑上多Agent,不然排查问题的时候,你会在多个角色的消息之间迷失方向。
如果让我给这条学习路线一个最朴素的建议,我会说:别急着上框架,也别急着搞多Agent,先把我上面那个几十行的循环自己敲一遍,加上日志、加上中断条件,再用你手头真实想解决的问题去替换里面的工具函数。我接触过不少开发者,凡是这样做下来的人,后面看再复杂的Agent架构都一目了然,因为万变不离那个循环。等到哪天你发现自己在反复处理消息管理、重试这类共性问题时,再去找框架借力,那时候你看框架的眼光会和现在完全不同。
最后再分享一个小技巧:给Agent的循环加上中间状态的可视化输出,也就是每一步模型想调什么工具、拿到了什么结果,都实时打印或渲染出来。这样调试起来非常直观,给人演示的时候效果也很好。代码不长,逻辑不玄,动手跑一遍比看十篇解析都有用。