Agent 这个词,这两年我被问得最多的一句话就是:“它跟直接在大模型对话框里聊天,到底差在哪?”问的人里有做产品的、有做后端的、也有刚转行想找个方向扎根的。大家的困惑其实高度一致:模型已经能写代码、能答问题、能总结文档了,为什么还要多套一层叫 Agent 的东西?我自己的理解很朴素——聊天框产出的是“文字”,Agent 产出的是“结果”。文字你还得自己去落地,结果是可以直接交付的。这篇文章我想把 Agent 这件事从概念到代码完整拆一遍,既讲清楚它为什么长成现在这个样子,也给出一个能手写跑通的最小实现,顺带把我在实际项目里踩过的坑摊开说。不管你是刚听说这个概念,还是已经在调工具调用调到怀疑人生,应该都能从里面捞到点能直接用的东西。
1. 先把话说透:Agent 到底是个什么东西
1.1 从一个真实的翻车场景说起
我印象很深的一次,某团队想做一个“自动整理周报”的功能。第一版做法非常简单:把一周的聊天记录、提交记录、任务看板导出丢给大模型,让它输出一份周报。效果嘛,第一周还行,第二周开始就出问题了——模型不知道哪些任务是本周完成的,哪些是上周遗留的,它会自己“脑补”时间线;也不知道某个任务到底卡在谁那里,因为那些信息散落在三个不同的系统里,压根没进 prompt。
问题的根子不在模型不够聪明,而在于这个任务本身需要“去查、去比对、去取数”这几个动作,而纯对话形态给不了模型这些能力。它只能基于你喂给它的那点上下文做语言层面的加工,喂漏了就编,喂多了就糊。
这就是我理解 Agent 的起点:当任务需要模型主动去获取信息、操作外部系统、并根据中间结果调整下一步时,纯对话就不够用了。Agent 本质上是给模型配了一套“感知—决策—行动”的闭环,让它能自己去补上缺失的那部分信息,而不是等着人喂。
1.2 Agent 与大模型的本质差别:从“会说话”到“会办事”
我把两者的差别整理成了一张表,基本能覆盖日常讨论里的绝大部分困惑:
| 维度 | 纯对话调用 | Agent |
|---|---|---|
| 输出形态 | 一段文本 | 一个完成的状态或产物 |
| 信息来源 | 训练知识 + 当次输入 | 训练知识 + 实时工具返回 + 记忆 |
| 执行次数 | 单次请求单次响应 | 多轮循环,直到满足终止条件 |
| 错误处理 | 无,错了就错了 | 可观察结果、可重试、可换路径 |
| 状态 | 无状态 | 有会话状态与中间过程 |
| 典型失败 | 幻觉、答非所问 | 死循环、工具误用、成本失控 |
看最后一行你会发现一个有意思的事:Agent 并没有消灭幻觉,它只是把幻觉的形态换了。纯对话的幻觉是“说错话”,Agent 的幻觉是“做错事”——调错工具、传错参数、在错误的假设上连走五步。这个认知很重要,它决定了后面所有的工程手段其实都在围绕“如何让错误尽早暴露、尽快纠正”来设计。
所以我不太喜欢那种“Agent 就是更聪明的大模型”的说法,这容易让人低估工程量。更准确的说法是:Agent 是一套以模型为决策核心的软件系统,模型只是其中那个“拿主意”的零件,剩下的记忆管理、工具协议、循环控制、失败兜底,全都是实打实的工程活。
1.3 一句话定义与四个必备要素
如果非要给一个定义,我会这么说:Agent 是一个能感知环境、自主规划步骤、调用外部能力、并根据反馈调整行为,最终达成给定目标的系统。
拆开来看,一个能跑起来的 Agent 通常需要四块积木:
- 规划(Planning):把“帮我做一份竞品分析”这种模糊目标,拆成“确定对比维度 → 检索资料 → 提取要点 → 成文”这样可执行的步骤序列。
- 记忆(Memory):短期记住这一轮任务里发生过什么,长期记住跨会话沉淀下来的知识与偏好。
- 工具(Tools):真正去读文件、查数据库、发请求、算数字的那双手。
- 执行与反思(Act & Reflect):跑完之后看一眼结果对不对,不对就调整重来。
这四块里,缺哪块都会退化成别的东西。缺工具,它就是个会分步思考的聊天机器人;缺记忆,它每轮都从零开始,稍微长点的任务必然跑偏;缺反思,它一条道走到黑,错了也不知道回头。
注意:很多号称“Agent”的产品其实只做了规划这一层,本质是“让模型先列个提纲再回答”,这类东西在演示里效果惊艳,一上真实任务就露馅,因为它的行动环节是空的。判断一个东西是不是真 Agent,看它有没有“根据行动结果改变后续行为”的能力,这一条比任何架构图都准。
2. 四块积木拆开看:每一块到底在干什么
2.1 规划:把大目标切成能落地的小步骤
规划这件事,听起来玄,实质上就是让模型在动手之前先输出一份步骤清单。为什么非要先输出?因为模型的推理是沿着 token 一步一步往前推的,如果一上来就要求它给最终答案,它没有“思考的草稿纸”,中间推理很容易塌掉。先让它把步骤写出来,等于强行给它腾了一块草稿区。
常见的规划做法有三档,复杂度递增:
第一档是隐式规划,也就是不显式输出步骤,靠模型自己在思维链里想。优点是省 token,缺点是不可控、不可观测,出了问题你不知道它想到哪一步歪的。
第二档是显式清单,让模型先输出一个编号列表,然后逐条执行。这档在实操里性价比最高,因为清单本身可以给人看、可以打日志、可以在中途插入人工确认。我做过的一个“批量处理表格”的 Agent 就用这档,它先把“读取 → 校验列名 → 清洗空值 → 分组统计 → 导出”列出来,人看一眼就知道它有没有理解错。
第三档是动态重规划,每执行完一步就回头看看清单还成不成立,不成立就改。这档是真正处理复杂任务必须的,但也是最容易失控的——重规划太频繁会陷入“反复改计划但一步没执行”的空转。
实操心得:我在给规划环节写提示词时,会强制要求模型给每一步标注“这一步需要用什么工具、预期的产出是什么”。这个约束看着啰嗦,但它能把大量“想了但做不了”的伪步骤提前筛掉。一个步骤如果说不清用什么工具,那它大概率就是一句正确的废话。
2.2 记忆:短期上下文和长期知识库根本是两回事
很多人把记忆理解成“把历史对话都塞进 prompt”,这是最容易踩的坑。记忆其实至少要分三层,各自的生命周期和处理方式完全不同。
第一层是工作记忆,也叫草稿板,生命周期就是当前这一个任务。它存的是“我已经查到订单号是 X”“用户偏好是简洁风格”这类中间结论。这层的关键设计是结构化:不要存成一坨自然语言,而是存成键值对或者 JSON。原因很实在——自然语言历史每轮都要重新读一遍,读得越多越容易串味;结构化字段可以精准取用,还能直接做程序化判断。
第二层是会话记忆,跨多轮对话但仅限同一用户同一会话。这层主要解决“指代消解”,比如用户上一句说“那个订单”,这一句说“帮我退了”,Agent 得知道“那个”指什么。
第三层是长期记忆,跨会话持久化,通常落在向量库或者结构化存储里。这层的难点不在存,在取。检索召回不准,塞进去的全是噪音,效果比不加还差。我一般的做法是双路召回:向量相似度召回一批,再按时间、标签、实体做一次过滤,两路取交集或者加权融合。纯向量召回在“用户上周提过的那个需求”这类时间敏感查询上几乎必然失手。
关于上下文长度,这里有个必须算清楚的账。假设系统提示 400 token、五个工具的描述 600 token、每轮工具返回平均 300 token、模型输出 150 token,那么第 10 轮时单次请求的上下文大约是 400 + 600 + 10 × 450 ≈ 5500 token。但注意,每一轮请求都要把之前所有历史重发一遍,所以 10 轮任务的总消耗不是 5500,而是接近 30000 token。按常见的每百万输入 token 两元档位估算,单次任务约 0.06 元。单看不多,可如果这个任务一天跑五千次,一天就是三百块,一个月近万。这就是为什么上下文管理不是优化项,而是必选项。
2.3 工具:给模型装上手和脚
工具这一层是整个 Agent 里最“工程”的部分,也是最容易出现“模型明明很聪明但就是调不对”的地方。它的底层机制其实不复杂:你把每个工具的名字、用途、参数结构用一段结构化描述塞进请求里,模型在需要的时候不再输出普通文本,而是输出一段特定格式的 JSON,声明“我要调哪个工具、传什么参数”。你的程序解析这段 JSON,真正去执行,再把结果作为一条新消息塞回去。
关键在于,工具描述的质量直接决定调用准确率,这一点怎么强调都不过分。我见过太多人把工具描述写成“查询订单信息”这五个字,然后抱怨模型老是调错。你换位想想,模型对这个工具的全部认知就是这五个字,它凭什么知道该在什么时候用、参数该传订单号还是用户 ID?
我总结的工具描述四件套,实测下来能显著降低误调率:
- 一句话说清用途,动词开头,比如“根据订单号查询订单的当前状态与物流信息”。
- 明确使用时机,写清“当用户询问订单进度、是否发货、预计到达时间时使用”。
- 明确禁用时机,写清“当用户询问退款政策时不要使用此工具,应使用政策查询工具”。这一条最容易被忽略,但收益最大。
- 参数逐个解释,包括格式、取值范围、示例值。日期就写“格式 YYYY-MM-DD,例如 2024-03-15”,别让模型猜。
再补一个细节:工具数量超过十个之后,模型的选错率会明显上升,因为它要在更多选项里做区分。这时候要么做工具分组(先选大类再选具体工具),要么做检索式工具加载(根据当前子任务只注入相关的三五个工具)。后者实现起来更麻烦但效果更好,我一般会等到工具数过十五个才上。
2.4 执行与反思:让循环真正收敛的那一下
反思环节是很多入门实现里被省略的部分,但它恰恰是区分“能跑”和“跑得稳”的分水岭。所谓反思,就是在拿到工具返回结果之后,不急着进入下一步,而是先做一次判断:这个结果符合预期吗?如果不符合,是该重试、换工具,还是该回头改计划?
最轻量的反思是规则校验,比如返回的是空列表、是错误码、是超时,直接触发重试或降级。这层不需要模型参与,纯代码判断,成本为零,收益极高。我几乎在所有项目里都会先加这一层。
再重一点的是模型自检,让模型看一遍工具返回,回答“这个结果是否足以支撑下一步”。这里有个小技巧:不要问开放式问题“结果对吗”,模型倾向于说对;要问封闭式的对比问题,比如“这个结果里是否包含订单状态字段?如果没有,说明原因”。把判断标准写进问题里,自检才有意义。
最重的是结果验证器,用独立的程序去验证最终产物,比如生成的代码跑一遍单测、生成的表格检查行数和列名、生成的链接实际访问一次。这层成本最高但最可靠,因为它是唯一不依赖模型自我判断的环节。
提示:反思环节千万别做成“无限自我批评”。我见过一个实现,模型每次都要评价自己上一步,评价完觉得不够好就重来,结果一个三分钟的活跑了二十分钟。给反思加硬性上限,比如整个任务最多重试三次,超了就降级输出当前最优结果并标注未完成部分,这比死磕到底体验好得多。
3. 架构选型:几种主流套路怎么挑
3.1 ReAct:边想边做,最像人干活的方式
ReAct 的思路可以概括成三个交替出现的动作:想一步、做一步、看结果。它不要求模型一开始就把全流程规划完,而是走一步看一步,根据每步的观察结果决定下一步。这个名字本身就是 Reasoning 和 Acting 的合成。
它的优点非常明显:适应性强。任务中途出现意外(比如查到的数据格式和预期不一样),它能当场调整,不需要推翻重来。人的工作方式其实也是这样,很少有谁能把一个复杂任务的所有步骤在动手前想得严丝合缝。
缺点同样明显:容易跑偏且难以预测。因为没有全局蓝图,模型可能在小细节上反复纠缠,也可能走了一条明显绕远的路。而且它天然是串行的,步数一多就慢。
我一般在两类场景用 ReAct:一是任务路径不确定、需要大量探索的,比如排查一个线上问题的根因;二是步数不多的轻量任务,比如三五步内能搞定的查询加工。
3.2 Plan-and-Execute:先画图纸再施工
这套思路是先让模型输出一份完整的执行计划,存下来,然后逐步执行,执行过程中可以选择性地更新计划。它把“想”和“做”在时间上分开了。
好处是可控、可观测、可干预。计划本身就是一份可以给人审阅的文档,你能在执行前就发现方向性错误,而不是等它跑了十步才发现南辕北辙。对于那种不可逆的操作(发邮件、改数据、下单),这一点几乎是刚需。
代价是灵活性下降。计划一旦定死,中途遇到没预料到的情况就得靠重规划机制兜底,而重规划做不好就会出现“计划改了八版,活一步没干”的尴尬。
我的经验是:步骤可枚举、结果可预期、涉及不可逆操作的任务,用 Plan-and-Execute;步骤模糊、需要边做边探的任务,用 ReAct。两者也可以混,外层用计划框住大阶段,每个阶段内部用 ReAct 自由发挥。
3.3 多 Agent 协作:什么时候值得上,什么时候是自找麻烦
多 Agent 是这两年被讲得最多、也最容易过度设计的方向。基本形态是让几个角色分工,比如一个负责拆解、一个负责执行、一个负责审核,彼此传递消息。
它确实能解决一些单 Agent 解决不了的问题。最典型的是上下文隔离:一个任务里有五个子领域,每个子领域都需要大量专业资料,全塞进一个上下文必然爆炸,拆成五个各自独立的 Agent 就清爽了。另一个是视角对抗,让一个 Agent 专门挑刺,能有效降低“自己审自己”的宽松倾向。
但我要泼一盆冷水:多 Agent 的调试成本是单 Agent 的数倍。问题出在消息传递上——A 传给 B 的信息少了,B 缺上下文;传多了,B 被干扰。而且一旦结果不对,你要沿着消息链条一层层回溯,日志不好看根本查不出来。
我的判断标准很直接:单 Agent 加好工具能解决,就别上多 Agent。只有当你明确遇到“上下文塞不下”或者“需要独立视角交叉验证”这两个具体瓶颈时,才值得付出这个复杂度。为了架构好看而上多 Agent,最后基本都是给自己挖坑。
3.4 三种架构的对照选择
| 对比项 | ReAct | Plan-and-Execute | 多 Agent 协作 |
|---|---|---|---|
| 适用任务 | 探索型、路径不确定 | 步骤明确、含不可逆操作 | 多子领域、需交叉验证 |
| 实现复杂度 | 低 | 中 | 高 |
| 可观测性 | 弱,过程在上下文里 | 强,计划可单独审阅 | 中,需专门做消息追踪 |
| 调试难度 | 低 | 中 | 高 |
| 成本 | 中,步数不可控 | 较低,计划可复用 | 高,多份上下文叠加 |
| 典型风险 | 绕远路、原地打转 | 计划僵化、重规划空转 | 信息丢失、责任不清 |
这张表我建议在做方案评审的时候直接摆出来,能省掉很多“我觉得多 Agent 更先进”的争论。
4. 手写一个最小可用 Agent:从零跑通
4.1 先把地基打好:接口、参数与预算
我不建议一上手就用重型框架。框架帮你封装了很多细节,但也把这些细节藏起来了,出问题时你会完全不知道该看哪。先用百来行原生代码把循环跑通,理解每一步在干什么,再决定要不要上框架——这个顺序我强烈推荐。
准备工作就三样:一个能调用的模型接口地址、一个请求凭据、一个 HTTP 客户端。下面这段是最小封装,把接口细节隔离在一个函数里,后面换供应商只改这一处:
import json import requests BASE_URL = "https://your-endpoint.example.com/v1" API_KEY = "your-key" MODEL = "your-model-name" HEADERS = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } def call_llm(messages, tools=None, temperature=0.2, timeout=60): payload = { "model": MODEL, "messages": messages, "temperature": temperature, } if tools: payload["tools"] = tools resp = requests.post( f"{BASE_URL}/chat/completions", headers=HEADERS, json=payload, timeout=timeout, ) resp.raise_for_status() return resp.json()["choices"][0]["message"]温度参数这里给 0.2 是有讲究的。Agent 场景下我们要的是稳定复现,不是文采。温度调高会让模型在同样的输入下选到不同的工具、传不同的参数,调试时你会被这种随机性折磨疯。等整套流程稳定了,再在“生成最终文案”这类环节单独调高温度。
再算一笔预算账,心里有个数:单次任务的 token 消耗 ≈ 初始上下文 × 轮次 + 累积历史。前面算过 10 轮约 30000 token。如果你设定最大 15 轮,那最坏情况约 50000 token,按每百万输入 token 两元算,单次最坏一毛钱。这个数字决定了你的超时和并发策略——如果你的场景一天一万次调用,那就必须把平均轮次压到 6 轮以内,否则成本会失控。
4.2 工具定义与注册表设计
工具部分我建议做成“注册表”结构,一个字典管住所有工具的描述和执行函数,新增工具只改这一处。这样能避免描述和实现脱节——那可是最常见的 bug 来源。
import datetime def get_order_status(order_id: str) -> dict: # 模拟实现,真实场景替换为数据库或接口查询 fake_db = { "A1001": {"status": "已发货", "carrier": "某物流", "eta": "2024-03-18"}, "A1002": {"status": "待付款", "carrier": None, "eta": None}, } if order_id not in fake_db: return {"error": f"订单号 {order_id} 不存在"} return fake_db[order_id] def calc_days_between(start_date: str, end_date: str) -> dict: try: s = datetime.date.fromisoformat(start_date) e = datetime.date.fromisoformat(end_date) return {"days": (e - s).days} except ValueError: return {"error": "日期格式必须为 YYYY-MM-DD"} TOOLS = { "get_order_status": { "schema": { "type": "function", "function": { "name": "get_order_status", "description": ( "根据订单号查询订单的当前状态、承运方与预计到达日期。" "当用户询问订单进度、是否发货、什么时候到货时使用。" "当用户询问退款政策、发票开具时不要使用此工具。" ), "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,形如 A1001,字母大写加四位数字", } }, "required": ["order_id"], }, }, }, "fn": get_order_status, }, "calc_days_between": { "schema": { "type": "function", "function": { "name": "calc_days_between", "description": ( "计算两个日期之间相差的天数,用于回答还剩几天、已经过了几天这类问题。" "涉及日期差计算时优先使用此工具,不要自行心算。" ), "parameters": { "type": "object", "properties": { "start_date": {"type": "string", "description": "起始日期,格式 YYYY-MM-DD"}, "end_date": {"type": "string", "description": "结束日期,格式 YYYY-MM-DD"}, }, "required": ["start_date", "end_date"], }, }, }, "fn": calc_days_between, }, } TOOL_SCHEMAS = [item["schema"] for item in TOOLS.values()]注意几个细节:工具函数永远不要抛异常给上层,出错就返回带 error 字段的字典。原因很实际——异常会让整个循环崩掉,而返回错误信息能让模型看到“哦,参数传错了”,下一轮自己改。这是把错误变成反馈的关键设计。另外日期类的活一定要给工具,别指望模型心算,闰年和月份天数它翻车概率不低。
4.3 主循环实现:所有魔法都在这个 for 里
主循环是整个 Agent 的心脏,逻辑其实短得惊人:
SYSTEM_PROMPT = """你是一个任务型助手。 规则: 1. 需要外部数据时必须调用工具,不要凭空猜测。 2. 每次只调用必要的工具,拿到结果后再决定下一步。 3. 工具返回错误时,先检查参数格式,最多重试两次。 4. 信息足够时直接给出最终答复,不要再调用工具。 5. 回答中的数字、日期、状态必须来自工具返回结果。""" def run_agent(user_query: str, max_steps: int = 8) -> str: messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_query}, ] called_signatures = set() for step in range(max_steps): msg = call_llm(messages, tools=TOOL_SCHEMAS) messages.append(msg) tool_calls = msg.get("tool_calls") if not tool_calls: return msg.get("content", "") for tc in tool_calls: name = tc["function"]["name"] raw_args = tc["function"]["arguments"] try: args = json.loads(raw_args) except json.JSONDecodeError: result = {"error": "参数不是合法 JSON,请检查格式后重试"} else: signature = f"{name}:{json.dumps(args, sort_keys=True, ensure_ascii=False)}" if signature in called_signatures: result = {"error": "相同参数已调用过,请更换参数或直接作答"} else: called_signatures.add(signature) handler = TOOLS.get(name) if not handler: result = {"error": f"工具 {name} 不存在"} else: result = handler["fn"](**args) messages.append({ "role": "tool", "tool_call_id": tc["id"], "content": json.dumps(result, ensure_ascii=False), }) return "已达最大步数,任务中止。请补充信息或缩小范围后重试。"这里面有三个不起眼但极其重要的设计。第一个是去重集合:called_signatures记录已经调过的“工具名 + 参数”,一模一样再来一次就直接驳回。死循环是 Agent 最高频的故障,这一行代码能解决其中大半。第二个是参数解析兜底:模型偶尔会输出不合法 JSON,不兜住就是整个循环崩掉。第三个是最大步数:必须有,且要给出明确的终止文案,而不是静默返回空字符串——用户看到“任务中止”至少知道该换个问法,看到空白只会以为系统坏了。
4.4 跑起来看效果:一次完整的执行记录
用一句“订单 A1001 预计哪天到,离今天还有几天”来测,实际跑出来的消息序列大致是这样:
第一步,模型判断需要两个信息,先调get_order_status,参数{"order_id": "A1001"}。工具返回{"status": "已发货", "carrier": "某物流", "eta": "2024-03-18"}。
第二步,模型看到预计到达日期,接着调calc_days_between,参数{"start_date": "2024-03-15", "end_date": "2024-03-18"}。工具返回{"days": 3}。
第三步,模型拿到天数,输出最终答复:“订单 A1001 已发货,承运方为某物流,预计 2024-03-18 到达,距离今天还有 3 天。”
整个任务花了三轮、两次工具调用,token 消耗大概在 2500 左右。这个量级是可以接受的。如果你发现自己的 Agent 在类似任务上跑了七八轮,那八成是提示词没写清“信息足够就作答”,或者工具描述让它不知道该调哪个,得回去改描述。
提示:调试阶段一定要把完整的 messages 序列打出来,尤其是每次工具调用的原始参数和返回。我见过太多人只看最终输出,结果排查半天才发现是模型把订单号传成了“A1001号”,模型没错,是它照着用户的说法原样搬了。
5. 常见问题与排查实录
5.1 死循环:同一个工具同参数调八遍
这是我踩过的第一个坑,也是最典型的一个。现象是模型反复调用同一个工具、传同样的参数,拿到同样的结果,然后继续调。
根因往往不是模型笨,而是结果没有提供新信息,但提示词里没告诉它“这种情况该停”。比如查一个不存在的订单,工具返回空,模型觉得“没查到,再查一次”,它不会自己意识到再查一百次也是空。
解决办法有三层,建议全上。第一层是前面代码里的签名去重,物理阻断。第二层是在工具返回里显式带上状态,把{"error": "订单号 A1001 不存在,请向用户确认订单号是否正确"}这样的引导语写进返回内容,模型看到这句通常就停了。第三层是在系统提示里写死规则:同一个工具连续失败两次后必须停止并说明原因。
补充一个变种:有些死循环不是调同一工具,而是在两个工具之间来回跳,A 的结果触发 B,B 的结果又触发 A。这种情况去重集合抓不住,得靠步数上限兜底,同时检查两个工具的描述是不是存在“互相推荐”的歧义,比如 A 的描述里写“数据不全时用 B 补全”,B 的描述里写“缺字段时用 A 补全”,那就必然死循环。
5.2 幻觉参数:模型编了一个不存在的 ID
现象是工具返回“记录不存在”,但用户明明给了正确的信息。
排查下来通常是两种情况。一种是模型做了字符串加工,用户说“我的订单,尾号 1001”,模型把order_id传成了1001,而真实格式是A1001。这属于参数说明不够严格。解决办法是在参数描述里把格式写死,同时在工具层做格式校验并返回明确提示:“订单号必须以字母开头,形如 A1001,请重新确认”。
另一种是模型在上下文里丢了信息。多轮对话下来,前面提到的订单号被后面的内容挤掉了,模型只能编一个。这种情况要么在每轮请求前把关键实体提取出来做成一个固定的“状态块”注进系统提示,要么用结构化的会话状态管理,别指望它自己记住。
还有一个隐蔽的情况:模型传了正确的值,但类型错了,传了数字而不是字符串。所以参数校验里类型检查也要做,别只检查值。
5.3 上下文爆炸与成本失控
任务跑得越长,上下文越大,成本涨得越快,而且到某个点之后模型的表现会不升反降——太多无关历史反而干扰判断。
我常用的三个手段,按见效速度排序:
第一,截断工具返回。绝大多数工具返回里真正有用的就几个字段,但接口动不动给你几百字。在工具函数里就把返回裁剪到必要字段,通常能省一半以上 token。
第二,压缩历史。保留最近 N 轮完整消息,更早的部分让模型自己总结成一段简短摘要。注意总结的时机是“超出阈值时触发”,不是每轮都总结,否则又是一笔开销。
第三,分级模型。规划、判断这类需要脑子的环节用好模型,格式化、摘要、简单抽取这类环节用便宜的小模型。这条在成本敏感场景下收益最明显,我做过的一个项目里,光这一项就把整体成本压下去了六成。
5.4 常见问题速查表
| 现象 | 大概率原因 | 处理动作 |
|---|---|---|
| 反复调同一工具同参数 | 结果无新信息、缺停止条件 | 签名去重 + 返回带引导语 + 提示词写停止规则 |
| 工具参数格式错误 | 参数描述不具体、缺示例 | 参数描述写格式与示例,工具层做校验并返回提示 |
| 该调工具却直接作答 | 提示词未强制、工具描述太弱 | 系统提示写明“需外部数据必须调工具” |
| 调错工具 | 工具描述太笼统、工具数过多 | 补禁用场景说明,工具超十个做分组或检索加载 |
| 上下文迅速膨胀 | 工具返回过长、历史未压缩 | 裁剪返回字段、超阈值压缩、分级模型 |
| 任务跑偏不回头 | 缺反思环节、无校验 | 加规则校验层 + 结果验证器 + 重试上限 |
| 输出数字与工具不符 | 模型自行加工 | 提示词要求数字必须来自工具返回,并做一致性校验 |
| 长任务中途超时 | 串行步数过多 | 拆子任务并行、提高单步超时、加断点续跑 |
6. 生产化落地:那些只有上线才会遇到的问题
6.1 权限边界:写操作必须有人把关
演示环境和生产环境最大的差别,是生产环境里的操作有后果。查询类工具出错顶多给个错误答案,写操作出错可能是改错了数据、发错了通知,很难撤回。
我的原则是按后果分级。只读类工具放开,让它自由调;有副作用的工具(写文件、发消息、改状态)必须加人工确认,或者至少加一道前置校验——比如金额超过阈值就转人工、批量操作超过 N 条就二次确认。
还有一个容易被忽略的点:工具的权限应该按调用方身份收敛,而不是按 Agent 的身份放大。Agent 不是什么超级账号,它替谁干活就该用谁的权限。否则一个普通用户通过精心构造的提问,就可能让 Agent 替他执行了越权操作。这个风险在真实项目里出现过不止一次,别掉以轻心。
6.2 幂等设计:重试不能变成重复执行
Agent 天然会重试,这是它的工作方式。但重试遇上非幂等操作就是灾难——用户收到三条一样的通知,或者一个订单被创建了两次。
做法很直接:每个有副作用的操作都带一个幂等键,通常是“任务 ID + 操作类型 + 关键参数”的哈希。执行前先查这个键有没有执行过,执行过就直接返回上次的结果。这个开销很小,但能避免绝大多数重复执行问题。
另外,工具本身的超时时间要和 Agent 的循环超时对齐。如果工具超时设得比循环还长,那循环都已经判定失败了,工具还在后台默默执行,结果就是一个“系统说失败但实际成功了”的诡异状态,排查起来极其痛苦。
6.3 评测与灰度:没有评测集就是蒙眼开车
这是我最想强调的一点。Agent 的行为是概率性的,你改一句提示词、加一个工具,很可能修好了 A 场景却弄坏了 B 场景,而你不测就完全不知道。
所以上线前必须有一份评测集。不用很大,二三十个真实场景就够,但要覆盖:正常路径、参数缺失、工具报错、多轮追问、以及你已知的那几个坑。每次改动跑一遍,看通过率。这个投入产出比高得吓人,我做过对比,有评测集的团队迭代速度快三倍以上,因为他们敢改。
灰度策略上,我会先放一小部分流量,重点看三个指标:平均步数(有没有异常增长)、工具调用失败率(有没有某类错误突然飙升)、任务完成率(用户是否真的拿到了结果)。前两个是技术指标,第三个是业务指标,三个一起看基本能定位到问题在哪一层。
6.4 观测:日志不是给你自己看的
Agent 的日志和普通服务不一样,它需要记录决策链路:每一轮模型看到了什么、决定了什么、为什么这么决定。没有这个,出问题时你只能看到一个最终错误,完全无从下手。
我一般会记录这几项:每轮的输入摘要(不是全文,太长)、模型的原始输出、工具调用的名称与参数、工具返回的关键字段、耗时。然后给整个任务一个 trace ID,把所有轮次串起来。这样排查时能看到完整的时间线,而不是一堆孤立日志。
还有一个实用的小设计:把那些“模型调用失败但最终成功了”的轮次单独标记出来。这些是模型在自我纠错的证据,也是你优化提示词的黄金素材——看看它在哪些地方容易走弯路,针对性补一句约束,往往一次就能减少一大批无效轮次。