做 AIAgent 应用开发的人,很多都卡在同一个问题上:单独调大模型时,模型表现得像个聪明人;一旦把它放进一个真正的任务流里,它就开始失控——答非所问、乱调工具、传错参数,甚至自己编一个结果出来。我陪同事调试内部自动化工具时,几乎每天都在收拾这种局面。这一篇是《大模型理论基础与应用》系列的第五篇,前四篇分别聊过模型原理、提示词组织、上下文机制和基础 API 调用参数,这一篇开始正式落到 Agent 层面,核心就一句话:怎么让大模型的输出从“一段话”变成“一个可执行的行动”。
这篇内容适合两类人。一类是刚开始做 AIAgent 应用的开发者,已经会调大模型接口,但不知道怎么把模型接进业务流程;另一类是已经写了几个简单 Agent 脚本、却总在真实场景里翻车的朋友。我会把工具调用背后的原理、接口设计细节、最小可运行的代码骨架,以及我在调试中踩过的坑一起讲清楚。你不需要有很深的算法基础,但需要能看懂 Python 风格的伪代码。
1. 从“能对话”到“能干活”的底层转变
1.1 聊天式交互与任务式交互的差别
很多人对 Agent 有个误解,以为 Agent 就是“在提示词里多加几句要求,外头套个 while 循环”。实际上,聊天式交互和任务式交互是两种完全不同的使用模型的方式。
聊天式交互的核心是“生成一段合理文本”。你问模型“帮我安排一场三点的会议”,它给你输出一句“好的,已经帮你安排好了”,你根本不知道它到底做了没有。因为它没有调用任何系统,它只是在预测文本。任务式交互的核心是“让模型产出一个可被程序解析的行动指令”。同样是安排会议,模型应该返回的是“调用哪一个工具、传什么参数”,然后把参数交给代码去真正执行会议室预订系统的 API。
这种差别就像你让一个实习生“去协调一下会议室”,和他按照流程单填写《会议室预定申请单》的区别。前者靠临场发挥,后者有明确字段、有校验规则、有交接动作。Agent 应用最忌讳的就是让模型在对话里“临场发挥”,因为它一旦开始临场发挥,就会出现看似合理、实际没有任何落地效果的输出。这是所有 Agent 稳定性问题的总根源。
1.2 第五篇在整个系列中的定位
前四篇其实都在解决一个问题:怎么让模型“更懂你”。你在提示词里写约束,它就更听约束;你调整温度参数,它就更稳定;你设计好上下文格式,它就更不容易跑偏。但这些方法都有天花板,因为文字本身就是模糊的。无论你怎么写“请返回一个 JSON”,模型都可能多解释一句“以上是我生成的 JSON”。
到了第五篇,解法变了:不再要求模型输出“人说的话”,而是要求模型输出“程序能直接用的指令”。这个转变的关键就是结构化输出和工具调用。我把结构化的东西想成一个“接口合同”,合同里规定模型只能说什么、不能说自由发挥的话、参数从哪里来、缺失怎么办。模型知道如果自己违反合同,后续程序根本没法跑,所以它在生成的时候会收敛得多。
如果你是从这一篇才开始看这个系列,也不用紧张。后面讲到工具调用时,我会把模型的请求和响应长什么样、工具参数怎么定义、程序的调度逻辑怎么写,都一步一步拆开。前面的基础主要是帮你理解“为什么模型这么不听话”,但现在的重点是“怎么让它不能不听话”。
1.3 开工前先划清的Agent边界
真正做 Agent 应用的人,第一件事不是写代码,而是划定边界。不是所有任务都适合让大模型直接拍板。涉及金钱支付、修改重要数据、对外发送消息、删除资源这一类动作,必须让模型只负责“提出建议方案”,由你的业务代码做最终权限判断。我见过最典型的事故,就是让 Agent 直接调用一个“删除订单”的接口,结果模型在排障时误判订单状态,几秒钟把一批数据清掉了。不是模型不聪明,是你给了它太大的扳手。
另一个边界是“失败边界”。你要提前想清楚:模型调用工具失败之后,它应该怎么办?是重试,还是换一个工具,还是直接放弃转人工?很多 Agent 没有想清楚这一点,结果模型在失败之后开始了漫长的自我修复式编造,它会把一个失败的工具结果写成“看起来成功”的状态,让后续流程继续走下去。这比直接报错更危险,因为报错你能看到,而“假成功”会让你误以为一切正常。在开始编码前,先把“哪些工具坚决不授权给模型”“失败以后谁能兜底”写下来,这是成本最低的稳定性措施。
2. 大模型调用进阶:结构化输出与工具调用
2.1 为什么不能让模型“随便说”
纯自由文本生成有一个致命问题:它永远无法保证格式。哪怕你在提示词里写了“请用三要素格式回复”,模型偶尔也会在开头加一句“好的,根据您的要求”,或者在结尾多补一行“如果您有其他问题请随时告诉我”。这些多余的词对聊天毫无影响,但对程序就是灾难,因为解析器会遇到意料之外的 token。
所以第一步是让模型走结构化输出通道。主流的模型服务商都有类似的约束生成机制,术语可能叫函数调用、工具调用,也可能叫 JSON 模式,但核心思想一致:模型不再是生成任意文本,而是从给定的候选工具里选择要调用的函数,然后按照工具的 JSON Schema 生成参数。你不需要信任它,因为结构约束本身就在模型侧生效,大部分合规实现里,模型要么不调用工具,要么返回一个合法可解析的参数对象。
这个机制的实际落点很有意思。你在聊天场景里问“明天北京天气怎么样”,模型可能会给你写一段“明天北京晴,气温 5~15 摄氏度,北风 3 级”。但在工具调用模式下,它会先回答“我不知道当前天气数据”,再生成一个get_weather的调用请求,参数里的 location 是“北京”,date 是明天的日期。由程序真正去天气服务取数据,拿回来之后再把数据塞回对话里,让它把结果组织成给用户看的文本。模型只负责决策和表达,不负责伪造事实。
2.2 工具调用的核心交互过程
我把一次完整的工具调用拆成七个环节。第一,把工具列表随用户请求一起发给模型。工具列表本质上是一段特殊的提示内容,描述了“你当前可用的能力有哪些”。第二,模型根据请求内容,判断自己是否需要某个工具。第三,如果它决定调用,就会在你给出的候选工具中选择一个,并按照工具定义的参数结构填写参数。第四,模型返回一条特殊的辅助消息,里面带一个 tool_calls 字段,指明函数名和参数。第五,你的代码解析这条消息,找到对应的本地函数执行。第六,函数运行后将真实结果以 tool 消息的形式返回给模型。第七,模型读取真实结果,决定下一步继续调用其他工具,还是生成最终答案。
这个过程中最容易搞反的事情是:很多人以为工具是模型去调用的。实际上模型从来不“执行”任何工具,它只负责发出调用意向,真正执行的是你的代码。这就像你在 App 里点了“确认支付”,真正扣款的是支付系统,不是那个按钮本身。理解这一点之后,很多安全设计就顺了——你完全可以只执行那些通过了校验的调用请求,把不合理的请求拦截在代码层。
从请求结构上说,每家厂商的字段命名略有差异,但基本都有工具名称、功能描述、参数 schema 这三部分。参数 schema 通常复用 JSON Schema 规范,我常用的写法是:
{ "name": "search_meeting_room", "description": "根据参会人数、时间段和所在楼层查找空闲会议室", "parameters": { "type": "object", "properties": { "capacity": { "type": "integer", "description": "至少能容纳的人数,例如:8" }, "start_time": { "type": "string", "description": "会议开始时间,格式为 YYYY-MM-DD HH:mm" }, "end_time": { "type": "string", "description": "会议结束时间,格式为 YYYY-MM-DD HH:mm" }, "floor": { "type": "integer", "description": "优先搜索的楼层,不填则搜索整栋楼", "enum": [1, 2, 3, 5, 8] } }, "required": ["capacity", "start_time", "end_time"] } }这段描述看起来普普通通,但里面的每一个字段都会直接影响模型在真实调用时的准确率。description 写得越具体,模型就越容易判断自己“该不该选这个工具、参数该填什么”。“至少能容纳多少人”和“人数”是两回事,“格式为 YYYY-MM-DD HH:mm”和“时间”也是两回事。只有把约束都写进协议里,模型才不需要靠猜。
2.3 工具参数Schema设计中的细节经验
工具参数设计是 Agent 开发里最容易被低估的事情。我见过很多项目,工具名起得含糊,参数描述只有一句话,结果模型经常把不该传的值传进来。这里我直接给几条反复验证过的心得。
第一,参数描述一定要给出“取值范围和格式示例”。别写“开始时间”,要写“会议开始时间,格式为 YYYY-MM-DD HH:mm,例如 2025-06-20 14:00”。因为模型不是真的在“理解”描述,它是在做模式匹配,示例就是最好的锚点。
第二,能用枚举限制的地方尽量用枚举。比如楼层如果只有 1、2、3、5、8 这五个,就把值写进 enum,把“不填则搜索整栋楼”写进描述。枚举一旦定义,模型在生成参数时就会被硬约束钳住,不会给你冒出个“B2 层”。
第三,required 字段必须和描述保持一致。不要只在描述里写“必填”,关键字段一定要出现在 required 数组里。很多模型的参数生成会优先保证 required 字段完整,如果你漏写了,它就可能觉得自己“可以省略”,结果就是程序这边接到一个缺少关键参数的调用。
第四,工具不是越多越好。工具列表里有几十个工具时,模型的选择准确率会明显下降。我的习惯是分两层:第一层先让模型从少数几个“路由器工具”里选,比如“查询范围”“执行操作”“找人工帮助”;第二层再根据路由结果加载对应的小工具列表。这相当于把一个大菜单拆成多级菜单,模型的选择压力会小很多。
3. 实现一个最小可用的ReAct Agent骨架
3.1 推理—行动—观察的主循环
ReAct 是目前最经典、也最适合入门的 Agent 执行范式,名字很玄乎,逻辑其实只有三层:推理、行动、观察。推理是你把当前任务状态和候选工具发给模型,让它思考“下一步该做什么,用什么参数做”;行动是模型调用工具;观察是把工具执行真实结果喂回给模型,让它基于真实情况继续推理。这个循环一直坚持到模型认为可以给出最终答案,或者达到执行步数上限。
为什么这个循环有效?因为每一步都有“观察”在纠正“推理”。模型最初的判断可能是错的,但工具的真实返回会把它拉回来。比如你让 Agent 查一下本月所有未结账单,模型第一反应可能是“本月=当前自然月”,但账单数据库里可能只有一个上月的未结账单。工具一返回为空,模型就会发现自己的假设有问题,然后继续追问或调整查询条件。没有观察环节的 Agent 就是纯粹的“自说自话”,它生成了一个自认为正确的行动,然后毫不在意地把它当成事实输出。
ReAct 和普通多轮对话的区别在于,它把“思考”和“事实来源”分开。思考可以随便说,但事实必须有工具结果支撑。这也是我在设计所有 Agent 时坚持的底线。
3.2 一个可运行的简化代码骨架
下面这段代码是我习惯的最小子集,为了让你看清主循环,我把各家 API 的差异全部封装进了model_client和一个工具执行函数里。实际生产代码里,你需要把complete方法换成你使用的模型服务商 SDK 或 HTTP 调用。
import json from datetime import datetime def execute_tool(name, arguments): tool_map = { "search_meeting_room": search_meeting_room, "book_meeting_room": book_meeting_room, "check_calendar": check_calendar, "send_notification": send_notification, } if name in tool_map: return tool_map[name](**arguments) return json.dumps({"error": f"unknown tool: {name}"}) def run_agent(user_request, tools, max_steps=8): messages = [{"role": "user", "content": user_request}] for step in range(max_steps): response = model_client.complete(messages=messages, tools=tools) message = response["message"] messages.append(message) # 模型没有产生工具调用,说明它认为可以给最终答案了 if not message.get("tool_calls"): return message["content"] for tool_call in message["tool_calls"]: fn_name = tool_call["function"]["name"] fn_args = json.loads(tool_call["function"]["arguments"]) result = execute_tool(fn_name, fn_args) messages.append({ "role": "tool", "name": fn_name, "content": result, "tool_call_id": tool_call["id"], }) return "已达最大执行步数,任务未能完成,已转为人工处理"这个骨架有几个要点。第一,每次循环都把模型返回的消息原样 append 到messages里,保证模型能看到自己上一步思考。第二,工具返回值必须放进role="tool"消息里,并且带上tool_call_id,很多模型是靠这个 id 把工具结果和之前的调用请求对齐的。第三,循环的退出条件有两个:模型认为不需要继续调用工具,或者步数超限。步数超限时,我选择返回一个明确的转人工信号,而不是让用户看到一句莫名其妙的循环。
3.3 停止条件、失败回退与多工具编排
真正把 Agent 放到业务里以后,步数上限只是最粗的保护线。你还要处理两类常见问题:一类是模型陷入局部循环,比如反复调用同一个工具、只是参数里改了一个无关紧要的字;另一类是模型把简单任务拆得过度复杂,本来一步能完成的查询,它非要先查 A、再查 B、最后再汇总。
我的处理办法是加一个“重复调用检测器”。如果同一个工具在连续三轮里出现两次以上,就把最新的工具结果直接广播给模型,并在消息里附加一条系统提示:“你已经重复调用同一个工具,请基于现有结果判断是否可以直接回答,不要重复试探。”这个方法在大多数场景下都能让模型冷静下来,因为绝大多数循环不是模型能力不够,而是它误以为“再多试一次就能找到更准的结果”。
多工具编排的另一个关键问题是顺序依赖。比如订会议室的流程里,必须先查空闲会议室,再订会议室,最后发通知。如果你把三个工具一次性全给模型,它可能先订了一个不存在的房间。更稳妥的做法是分阶段给工具:第一阶段只给查询类工具,不展示任何写操作;等模型返回了具体的会议室 ID 和足够信息,再在下一轮消息里注入预订工具。这个做法叫“分层工具可见性”,它比单纯在提示词里写“请先查询再预订”可靠得多。我在生产项目里用过好几次,效果比任何提示词约束都突出。
4. 真实场景中的稳定性问题与排查手法
4.1 模型“编造”工具观察结果怎么办
操蛋的是,编造工具结果在真实 Agent 里太常见了。你让模型调用天气查询,它没有真正把查询结果拿到,就直接在回复里写“北京晴,5~15 度”。为什么会出现这种情况?因为有些模型服务商在实现工具调用时,允许模型在未收到任何工具返回的情况下继续生成文本。更常见的是,代码在把工具结果写回对话时,没有把旧的假结果清理干净,模型就把上一轮自己编的内容当了真。
解决这个问题只有一条路:在代码层做硬隔离。工具执行结果完全不经过模型生成,而是由你的业务代码从真实数据源获取,再以 tool 消息塞回对话。同时要让模型明白,任何未经过工具返回的数据都不是可靠数据。如果模型在没有任何工具返回的情况下直接说“查到了结果”,就用一条系统提示打断它,要求它先调用对应工具。宁可多跑一轮,也不能让假结果参与后续决策。
我也习惯在工具结果前加一个状态前缀,比如TOOL_OK:或TOOL_ERROR:。这样模型一眼就能判断这次工具调用有没有成功,不会把报错误读成正常返回。这个小习惯帮我解决过非常多“看起来正常、实际上数据不对”的案例。
4.2 上下文污染与循环抖动
上下文污染是 Agent 长期运行的慢性病。典型症状是:前面某一次工具返回了一段有误导性的数据,模型之后每一轮都会被那段数据影响,哪怕后面的工具已经返回了正确结果。这个问题本质上是“旧结果优先级高于新结果”,因为模型会把上下文里的所有信息等价看待,时间靠后的信息未必更可信。
我的处理方法是给消息队列引入“关键状态覆盖”。每次工具返回真实结果后,我会在消息里插入一段专门的“当前任务事实摘要”,用非常明确的话写清楚当前已确认的事实。比如“当前已确认:会议室的 ID 为 102,时间是 15:00,尚未预订”。模型在看到这段摘要后,会优先依据摘要推进,而不是自己去翻前面的零散结果。
循环抖动的另一个常见原因是提示词过载。有些 Agent 会在循环里把之前所有的用户请求、工具说明、历史结果全部原样保留,消息一长,模型就开始迷路。解决方式是给 messages 列表加一个“滑动窗口”,保留最近 N 轮交互,把更早的内容压缩成一段简短摘要。窗口大小我一般控制在 20 条消息左右,超过就触发摘要生成,避免模型被大量无关上下文淹没。
4.3 工具参数幻觉与合法性校验
模型在生成工具参数时也会“幻觉”,最常见的是枚举值传错、时间格式传错、必填字段缺失。比如搜索会议室时,模型把“5 楼”传成了“5028 楼层号”,或者把日期传成了“明天”。这些问题模型自己未必意识到,但你的业务接口一定会得到一条报错。
我的经验是:千万不要把参数校验的责任交给模型,能交给代码的就交给代码。每次从消息里解析出 tool_calls 后,我都会做一次 JSON Schema 校验,不符合 schema 直接丢弃这轮调用,并返回一条 tool 错误信息让模型重写参数。这个校验循环通常最多两轮就能让模型生成合法参数,因为它能看到错误回执,知道自己在哪个字段上出了问题。
还有一个很隐蔽的坑:工具参数里有敏感字段时,模型可能把不该暴露的信息当成参数传出去。比如一个“发送通知”工具,模型可能把内部联系人的手机号当作通知内容的一部分。这在很多项目里会引发数据安全隐患。我的建议是,在工具描述里明确区分“参数”和“静态配置”,把通信录映射放在代码层,而不是靠模型从上下文里翻出来。
4.4 模型能力边界与架构取舍
不是所有问题都能靠优化提示词和工具定义解决。有时候模型确实会在一个很简单的判断上反复出错,比如分不清“未结单”和“已结单”,或者把两个相似的客户名称搞混。这时候别硬撑,直接在架构上做取舍。
我的取舍原则很简单:把 Agent 当“路由中心”而不是“业务大脑”。所有需要精确计算、严格校验、幂等控制的逻辑,都不要指望模型自己完成。让模型做的是判断“该走哪条流程、该调用哪个模块”,具体计算交给确定性代码。举个例子:你要做一个报销审核 Agent,模型只负责判断单据是否齐全、是否合乎规则,最后计算报销金额的步骤,直接调用一段固定公式,而不是让模型算。模型算错一个小数点,后面整个流程都没法收拾。
另一个取舍是模型级别。对于复杂路由和工具选择,使用强模型;对于简单分类和摘要,使用轻量模型或专用规则。混合架构的维护成本稍高,但回报非常明显。我在一个客服流程项目里,用强模型做意图判断,轻量模型做话术生成,结果准确率比单一模型高了不少,响应成本却降了三分之一。
5. 让Agent进入生产环境的四个扩展方向
5.1 短期记忆:滚动窗口与摘要压缩
Agent 在多轮对话场景里最缺的就是记忆。用户说“刚才那个会议室的事,帮我发个通知”,模型如果看不到“刚才那个会议室”的上下文,就根本不知道你在说什么。最朴素的方案是把所有对话历史都塞进上下文,但 token 成本会把你拖垮。我用了很多项目之后,总结出两个实用策略。
第一个是滚动窗口。只保留最近 8~10 轮模型消息和工具结果,其余消息不在下一次请求里携带。这个方案适合任务单一、上下文耦合度不高的场景,比如查天气、查快递。
第二个是摘要压缩。当消息数量超过阈值时,调用一次模型,把前面的对话整理成一份“任务状态摘要”,用这段摘要替换旧消息。摘要里必须包含用户的核心诉求、已确认的关键事实、待办动作、还有没有完成的依赖。这里有个小坑:摘要生成时一不小心会把关键细节丢掉,所以我会在代码里强制把工具返回值中的 ID、时间、状态单独列出,不让摘要模型用自己的话改写这些关键字段。
5.2 外部知识:让Agent能查资料再回答
纯靠模型内置知识的 Agent 有一个硬伤:知识截止日期和数据不准确。解决方式是把外部知识源封装成一个检索工具,比如搜索知识库、查 FAQ、读文档。和普通工具不同的是,检索工具的返回内容通常很长,直接塞进上下文会占用大量 token。
我的做法是给检索结果做“片段化预处理”。回到 Agent 的是按相关度排序的片段,每个片段带标题和出处,长度控制在几百字以内。这样模型既能看到足够的信息支撑回答,又不会被一整篇文档淹没。检索工具还要设计一个“无结果”分支:如果知识库里确实没有相关内容,工具返回NO_RESULT,模型必须转人工或明确说明找不到,而不是自编一个回答。这个分支设计过的话,很多幻觉问题都能被提前拦截在知识层。
5.3 权限与审批:危险动作必须过一道闸
Agent 一旦接入写操作,就必须有权限体系。我强烈建议把工具分成三个等级。第一级是只读工具,模型可以随意调用;第二级是普通写工具,模型可以直接执行,但执行前代码要校验业务规则;第三级是高危工具,模型只能返回“待审批请求”,必须由人工点确认才真正执行。发消息、改数据、发生支付、删除资源,全部放第三级。
有人觉得这样会让 Agent 变慢,但真实生产里,慢一点远好于出事。我有个习惯是在工具描述里直接写清楚“此操作需要审批”。模型看到这句话之后,会倾向于生成一个审批请求而不是自作主张执行。同时,代码层要把审批动作设计成幂等:同一个危险的 tool_call 重复提交时,不会真的执行两遍,只会提醒用户“该请求已存在”。这个细节避免了不少重复退款或者重复删除的事故。
5.4 可观测性:把每轮行为记录成回放日志
Agent 和普通接口最大的区别是它有无穷多的分支路径。你不能只记录“用户问了什么、最终答了什么”,因为中间任何一步错误,最终结果可能看起来还是正常的。可观测性的核心是“完整回放”。
我现在要求每个 Agent 项目至少记录这些字段:会话 ID、步骤序号、模型发送给用户的全部消息、模型工具调用请求、工具名称、工具入参、工具返回原始内容、模型最终答复、token 消耗、耗时。这些数据不仅要存,还要能按会话拉出来逐条看。排查问题时,我一般先看工具返回内容,再看模型入参,然后才看 prompt。九成问题都能在工具返回里找到答案,要么是返回结构不对,要么是返回内容有误导,要么是代码把返回内容切坏了。
我在内部还养成了一个习惯:每隔一段时间把历史会话里的工具调用日志抽出来,人工看一遍“模型在哪些场景下反复犯错”,然后针对性优化工具描述或增加校验规则。这个流程成本不高,但比任何测试集都贴近真实业务。
最后分享一个我自己的做法,也是踩过好几次坑之后形成的执念:每个 Agent 项目落地时,必须留两个文件。一个是工具清单,把每个工具的 name、description、参数 schema 当合同一样维护,每次改工具定义都要走同步更新;另一个是会话回放日志,让任何一次线上异常都能被重新演一遍。工具的定义就是 Agent 的地基,日志就是它的体检报告。没有这两样东西,Agent 跑得越久,隐患就积累得越深。希望这篇第五篇能帮你把模型从“会说话”推到“会干活”,也祝你的 Agent 在真实业务里少出点幺蛾子。