
1. 为什么“七要素”讲不清 Agent 的工程实现市面上讲 AI Agent 的文章十个里有八个会先甩出一张架构图然后告诉你 Agent 由“规划、记忆、工具、执行”这几大块组成。看完之后你确实觉得懂了但一打开 IDE 准备写代码发现根本不知道第一行该敲什么。问题出在哪出在“要素”是静态的切面而 Agent 是动态的运行时。我最早接触 Agent 开发的时候也是被各种“要素论”带着走。把 LLM 当大脑、把工具当手脚、把记忆当笔记本这个比喻本身没错但它解释不了工程中最要命的那几个问题LLM 什么时候该停下来工具调用的结果怎么塞回上下文循环到第几轮该强制退出这些问题的答案不在“要素”里而在“决策点”上。所谓决策点就是 Agent 运行过程中必须做出判断的那些时刻。一个 Agent 从收到用户输入到给出最终回复中间会经历至少七个关键决策要不要调用工具、调哪个工具、参数怎么填、结果怎么解析、要不要继续循环、上下文怎么裁剪、什么时候终止。这七个决策点串起来才是 Agent 真正的工程骨架。七要素告诉你 Agent 由什么组成七个决策点告诉你 Agent 怎么跑起来。前者是解剖图后者是行车记录仪。这篇文章面向的是已经了解 LLM 基本调用、准备动手搭一个能跑通的 Agent 的开发者。我不会只给你概念而是把每个决策点的实现逻辑、常见坑、以及我在实际项目中踩过的教训都摊开讲。读完你至少能明白一件事Agent 的复杂度不在于模型多强而在于这七个决策点你处理得够不够细。提示本文讨论的 Agent 特指基于 LLM 的对话式智能体不涉及强化学习训练出来的决策智能体也不涉及多智能体协作编排的进阶话题。先把单 Agent 的循环跑通比什么都重要。2. 第一个决策点这轮到底要不要调工具Agent 和普通 Chatbot 最本质的区别就是它能在对话过程中决定“我要去查一下”或者“我要去算一下”。但这个决定不是白来的它直接决定了你这轮响应的延迟、成本和可靠性。我见过不少新手写的 Agent不管用户问什么都先调一遍工具结果用户只是说了句“你好”Agent 也去查了一遍天气 API纯属浪费。2.1 工具调用的触发信号从哪里来目前主流的做法有两种。一种是靠 LLM 自己判断你在 system prompt 里写清楚“当用户的问题需要实时数据或外部计算时调用相应工具”然后让模型输出一个结构化的 tool_call 指令。另一种是在代码层面做前置路由用规则或小模型先判断意图再决定要不要把工具列表传给 LLM。这两种方式各有取舍。纯靠 LLM 判断的好处是灵活用户说“帮我看看明天出门要不要带伞”模型能自己推理出需要调天气工具。坏处是不稳定同一个问题换个问法模型可能就不调了。代码层前置路由的好处是可控你可以针对特定关键词或意图做硬编码坏处是维护成本高意图一多规则就爆炸。我实际项目里的做法是混合先用一个轻量的意图分类器做粗筛把明显不需要工具的对话比如闲聊、追问、确认直接拦掉剩下的再交给 LLM 做精细判断。这样既控制了成本又保留了灵活性。意图分类器不需要多强一个微调过的小模型甚至关键词匹配都能顶一阵子。2.2 不调工具的时候 Agent 在干什么这里有个容易被忽略的点不调工具不等于什么都不做。Agent 在不调工具的时候实际上是在做“基于已有上下文的直接生成”。这时候它的行为跟普通 Chatbot 没区别但你的工程实现上要注意不要因为走了不同分支就产生两套完全不同的响应格式。我踩过的坑是调工具的分支返回的是结构化 JSON不调工具的分支返回的是纯文本结果前端解析的时候经常报错。后来统一成所有响应都走同一个输出协议工具调用的结果也包装成文本再返回问题才解决。这个决策点的工程意义在于你要保证无论走哪条路下游拿到的数据格式是一致的。注意如果你的 Agent 支持多轮对话还要考虑“上一轮调了工具这一轮用户只是追问”的情况。这时候不应该重新调工具而是应该基于上一轮的工具结果继续推理。判断依据是上下文里是否已经存在可用的工具返回结果。3. 第二个决策点在多个工具之间怎么选当你的 Agent 挂载了超过三个工具之后选错工具就成了最常见的故障。我见过一个客服 Agent用户问“我的订单到哪了”它去调了“修改收货地址”的工具原因仅仅是两个工具的描述里都出现了“订单”这个词。这不是模型笨是工具描述没写好。3.1 工具描述的质量决定选择准确率工具描述不是写给人看的文档是写给 LLM 看的“使用说明书”。我总结了一个工具描述的三段式模板第一句说这个工具做什么第二句说什么时候该用第三句说什么时候不该用。举个例子一个查询订单状态的工具描述应该写成“查询指定订单的当前物流状态。当用户询问订单进度、配送位置、预计送达时间时使用。当用户要修改订单信息或取消订单时不要使用此工具。”这个“不要使用”的第三句是很多人会漏掉的。LLM 在多个相似工具之间做选择时负向约束往往比正向描述更有效。你告诉它“什么时候别用”比告诉它“什么时候用”更能减少误选。3.2 工具数量超过七个之后怎么办有个经验数据当工具数量超过七个LLM 的选择准确率会明显下降。这不是模型能力问题是上下文里工具描述太多注意力被稀释了。解决办法有两个方向。一是做工具分组先让 LLM 选组再在组内选具体工具。二是做工具检索把用户 query 和工具描述都向量化先召回最相关的三到五个工具再让 LLM 从中选。我倾向于第二种因为实现简单且效果稳定。具体做法是维护一个工具描述的向量库每次用户输入进来先做一次相似度检索把 top-k 的工具塞进 prompt。这样 LLM 每次面对的候选工具不超过五个选择准确率能拉回来不少。代价是多了一次向量检索的延迟但相比选错工具导致的返工这个代价完全值得。策略适用场景优点缺点全量工具列表工具数 ≤ 5实现简单无额外延迟工具多了准确率下降工具分组工具有明显类别层级清晰可解释需要维护分组逻辑向量检索召回工具数 7准确率高扩展性好增加检索延迟和向量库维护3.3 工具选择失败后的兜底逻辑再好的选择逻辑也会有选错的时候。关键是选错之后怎么办。我的做法是在工具执行层加一层校验每个工具在执行前先检查参数是否满足前置条件不满足就返回一个结构化的错误信息而不是直接抛异常。这个错误信息会回到 LLM 的上下文里让它有机会重新选择或向用户澄清。比如用户说“帮我订一张明天去北京的票”Agent 选了“查询航班”工具但没填出发城市。工具返回“缺少出发城市参数请向用户确认”。LLM 收到这个结果后下一轮就会问用户“请问您从哪个城市出发”。这比直接报错给用户看要优雅得多。4. 第三个决策点工具参数怎么填才不出错参数填充是 Agent 工程里最琐碎但也最容易出 bug 的环节。LLM 生成参数的本质是“根据上下文猜”猜对了皆大欢喜猜错了就是一场灾难。我经历过最离谱的一次是用户说“帮我给张三发个消息”Agent 把消息内容填成了“张三”两个字因为它在参数映射时把收件人名字误当成了消息体。4.1 参数 schema 的设计原则工具的参数定义要用 JSON Schema这是常识。但很多人写 schema 的时候太随意导致 LLM 理解偏差。我的经验是三条第一每个参数都要有 description而且 description 里要写清楚格式要求比如“日期格式为 YYYY-MM-DD”第二能枚举的参数不要用自由文本比如“优先级”就应该是 enum: [high, medium, low]第三必填参数和可选参数要严格区分required 数组里只放真正必须的。还有一个细节参数名不要用缩写。你写addrLLM 可能理解成 address也可能理解成 addressee。写全address就没歧义了。同理qty不如quantitydesc不如description。多打几个字符省下的是调试时间。4.2 参数校验应该放在哪一层参数校验有两个位置可选一是 LLM 生成参数后、工具执行前在代码层做一次校验二是工具内部自己做校验。我的建议是两层都做但职责不同。代码层做格式校验比如日期格式对不对、枚举值在不在范围内、必填项有没有缺。工具内部做业务校验比如查询的订单号是否存在于数据库、用户是否有权限操作这个资源。代码层校验失败时不要直接抛异常而是把校验错误信息返回给 LLM让它重新生成参数。这相当于给 LLM 一次“改作业”的机会。我实测下来大部分参数格式错误都能在第二轮修正。如果连续两轮都校验失败再降级为向用户澄清。4.3 多参数之间的依赖关系怎么处理有些工具的参数之间存在依赖关系。比如“查询航班”工具如果你填了“经停城市”参数那么“直飞”参数就必须为 false。这种跨参数的约束JSON Schema 本身表达不了需要在工具描述里用自然语言写清楚或者在代码层做联合校验。我的做法是在工具描述的最后加一段“参数约束”说明用自然语言列出所有跨参数规则。LLM 对自然语言约束的遵守程度实测比想象中好。当然代码层的联合校验也不能省这是最后一道防线。提示如果你的工具参数里包含用户敏感信息比如身份证号、手机号记得在日志里做脱敏。我见过因为日志打印了完整参数导致信息泄露的案例这个坑踩一次就够了。5. 第四个决策点工具返回结果怎么塞回上下文工具调完了结果拿到了然后呢直接原样塞回 LLM 的上下文如果你这么做迟早会遇到上下文爆炸的问题。一个查询数据库的工具返回了 200 条记录每条记录 50 个字段这一下就是上万 token。LLM 的上下文窗口再大也扛不住这么造。5.1 结果裁剪的三种策略第一种是截断只取前 N 条记录或前 M 个字符。简单粗暴但可能丢掉关键信息。第二种是摘要用另一个 LLM 调用把工具结果压缩成一段简短描述。效果好但增加成本和延迟。第三种是结构化提取根据当前对话的意图只提取相关字段。我常用的是第三种加第一种的组合先根据意图提取相关字段如果提取后还是太长再做截断。比如用户问“我的订单到哪了”工具返回了订单的完整信息我只提取“物流状态”和“预计送达时间”两个字段塞回上下文其余字段丢弃。这样既保留了关键信息又控制了 token 消耗。5.2 工具结果的格式对 LLM 理解的影响工具返回的结果格式直接影响 LLM 能不能正确理解。我试过返回纯 JSON也试过返回自然语言描述实测下来自然语言描述的理解准确率更高。比如同样是订单状态返回{status: shipped, eta: 2024-01-15}和返回“订单已发货预计 2024年1月15日 送达”后者 LLM 的后续推理明显更顺畅。所以我现在的一个习惯是工具内部返回结构化数据但在塞回上下文之前做一层“自然语言化”的转换。这个转换不需要 LLM用模板拼接就行。比如“订单状态{status}预计送达{eta}”。这样既保留了结构化的准确性又让 LLM 更容易理解。5.3 工具报错信息怎么处理工具执行失败是常态网络超时、权限不足、资源不存在各种情况都有。关键是怎么把错误信息传递给 LLM。我的原则是错误信息要包含“发生了什么”和“可以怎么办”两部分。比如“查询失败订单号不存在。请向用户确认订单号是否正确”。只告诉 LLM“查询失败”是不够的它不知道下一步该干嘛。加上“请向用户确认订单号”之后LLM 就知道该走澄清分支了。这个细节看起来小但对 Agent 的自主恢复能力影响很大。6. 第五个决策点循环该继续还是该停Agent 的核心是一个循环思考、行动、观察、再思考。但这个循环不能无限转下去。我见过一个 Agent 因为工具一直返回空结果LLM 一直重试转了 30 多轮才因为超时被强制终止烧了一堆 token 不说用户体验也极差。6.1 循环终止的四个条件我在工程里会设置四个终止条件满足任意一个就退出循环。第一LLM 输出了最终答案没有新的工具调用请求。第二循环轮数达到了预设上限我一般设 10 轮复杂场景最多 15 轮。第三连续两轮的工具调用结果完全相同说明陷入了死循环。第四累计 token 消耗超过了预算阈值。这四个条件里最容易被忽略的是第三个。LLM 有时候会陷入“调工具、得到结果、觉得不对、再调同一个工具”的循环。检测到连续两轮结果相同就强制退出能省下不少冤枉钱。6.2 循环轮数上限设多少合适这个问题没有标准答案取决于你的任务复杂度。我统计过自己项目里的数据简单问答类任务平均 1.2 轮就结束需要多步推理的任务平均 3.5 轮最复杂的任务也没超过 8 轮。所以 10 轮的上限对绝大多数场景都够用了。如果你不确定可以先设一个宽松的上限比如 15 轮然后在上线后统计实际轮数分布再根据 P99 值来调整。我一般会把上限设在 P99 轮数的 1.5 倍左右既不会误杀正常请求又能拦住异常循环。6.3 循环过程中的上下文管理每轮循环都会往上下文里追加新的消息上下文会越来越长。如果不做管理到第五六轮的时候可能就撑爆窗口了。我的做法是维护一个“滑动窗口 关键信息锚点”的上下文结构。滑动窗口保留最近 N 轮的消息关键信息锚点则保存最初的任务描述和已经确认的事实。具体实现上我会在上下文里单独维护一个“任务状态”区域记录当前任务的目标、已经收集到的信息、还缺什么。每轮循环开始时把这个状态区域放在 prompt 的最前面让 LLM 始终记得自己要干什么。这样即使滑动窗口把早期的消息挤掉了任务的核心信息也不会丢。终止条件触发时机处理方式最终答案输出LLM 无工具调用请求正常返回轮数上限达到预设轮数返回当前最佳结果并提示结果重复连续两轮工具结果相同中断循环并返回Token 超预算累计消耗超阈值中断并降级处理7. 第六个决策点上下文窗口快满了怎么裁上下文管理是 Agent 工程里最不性感但最重要的工作。LLM 的上下文窗口是有限资源而 Agent 的循环机制天然会消耗大量上下文。你不管理它它就管理你——以报错的方式。7.1 哪些内容必须保留哪些可以丢我的优先级排序是这样的System prompt 必须保留这是 Agent 的行为准则。当前任务的目标描述必须保留这是方向。最近两轮的工具调用和结果必须保留这是短期记忆。已经确认的关键事实必须保留比如用户告诉你的订单号、地址等信息。其余的历史对话可以按时间远近逐步丢弃。这个排序背后的逻辑是Agent 需要知道“我是谁、我要干什么、我刚做了什么、我知道了什么”。至于“我三分钟前说过什么客套话”丢了就丢了。7.2 摘要压缩的具体操作当上下文实在装不下的时候就需要做摘要压缩。我的做法是把最早的那部分对话拿出来用一个单独的 LLM 调用生成一段摘要然后用摘要替换掉原始对话。摘要的 prompt 我会写成“请用不超过 100 字总结以下对话中与当前任务相关的关键信息忽略寒暄和重复内容。”这个操作的成本是一次额外的 LLM 调用但相比上下文溢出导致的请求失败这个成本是值得的。我一般会在上下文使用率达到 70% 的时候触发摘要压缩留 30% 的余量给后续循环。7.3 上下文裁剪的时机选择裁剪的时机也很关键。我试过在每轮循环开始前裁剪也试过在上下文快满的时候裁剪。实测下来后者更好。因为每轮开始前裁剪可能会把上一轮刚产生的有用信息裁掉。而在快满的时候裁剪能最大化利用每一轮的信息价值。具体实现上我会在每次准备调用 LLM 之前检查当前上下文的 token 数如果超过窗口的 70%就触发裁剪流程。裁剪后再调用 LLM。这样既保证了不溢出又尽量保留了完整信息。注意不同模型对 token 的计算方式不同中文和英文的 token 比例也不一样。不要凭感觉估算用 tokenizer 实际算。我见过因为估算偏差导致线上请求失败的案例这个坑不值得踩。8. 第七个决策点什么时候该向用户求助Agent 不是万能的有些问题它自己搞不定。这时候是硬着头皮瞎编一个答案还是老老实实向用户求助我的原则是宁可承认不知道也不要编造一个错误答案。因为编造的答案会污染后续对话用户基于错误信息继续追问整个对话就崩了。8.1 什么情况下应该触发求助我设定了三种触发求助的场景。第一连续两轮工具调用都失败且失败原因不是参数格式问题。第二LLM 输出的答案置信度低这个可以通过让 LLM 自评置信度来实现。第三用户的问题超出了所有已挂载工具的能力范围。第三种情况最常见。用户问了一个你的 Agent 根本没有工具能回答的问题这时候正确的做法是告诉用户“我目前没有能力处理这个问题”而不是让 LLM 自由发挥。自由发挥的结果往往是幻觉而幻觉在 Agent 场景下的危害比普通对话更大因为用户会以为 Agent 真的去查了。8.2 求助话术的设计求助话术不是简单说一句“我不知道”。好的求助话术应该包含三部分承认能力边界、说明已经尝试了什么、给出替代建议。比如“我目前没有查询实时汇率的工具无法直接回答您的问题。您可以尝试在银行 App 中查看或者告诉我您想换算的金额和币种我帮您做数学计算。”这个话术的好处是它没有把用户堵死而是给了一个替代路径。用户即使得不到直接答案也知道下一步该干嘛。这比冷冰冰的“我不知道”体验好得多。8.3 求助之后的对话怎么继续用户收到求助信息后可能会换一种问法或者提供更多信息。这时候 Agent 需要能够接住。我的做法是在求助之后把“当前任务未完成”这个状态记录下来下一轮用户输入进来时优先检查是否是对上一个未完成任务的补充。比如用户说“那帮我算一下 100 美元等于多少人民币”Agent 检测到这是对上一个汇率问题的补充就会直接调用计算工具而不是重新走一遍意图识别。这个状态传递的机制能让多轮对话的体验连贯很多。9. 把七个决策点串起来一个最小可运行 Agent 的骨架讲了七个决策点现在把它们串起来看。一个最小可运行的 Agent代码结构大概长这样外层是一个 while 循环循环内部依次执行“构建上下文、调用 LLM、解析输出、判断是否调工具、执行工具、处理结果、检查终止条件”。七个决策点分别对应循环里的不同判断分支。9.1 核心循环的伪代码结构def run_agent(user_input, max_turns10): context build_initial_context(user_input) for turn in range(max_turns): # 决策点6上下文裁剪 context trim_context_if_needed(context) # 调用 LLM response call_llm(context) # 决策点1是否调工具 if not response.has_tool_call: # 决策点7是否需要求助 if response.confidence THRESHOLD: return ask_user_for_help(response) return response.content # 决策点2选了哪个工具 tool select_tool(response.tool_call) # 决策点3参数校验 params validate_params(response.tool_call.params) if not params.valid: context.append(error_message(params.error)) continue # 执行工具 result execute_tool(tool, params) # 决策点4结果处理 processed_result process_result(result) context.append(processed_result) # 决策点5循环终止检查 if should_terminate(context, turn): break return fallback_response(context)这段伪代码里每个决策点都对应一个函数调用或条件判断。实际工程中每个函数内部还有大量细节但骨架就是这个样子。你把每个决策点的逻辑都实现清楚Agent 就能跑起来了。9.2 每个决策点的测试策略Agent 的测试比普通函数难因为 LLM 的输出是不确定的。我的做法是分层测试。决策点1和2用 mock LLM 测试固定 LLM 的输出验证分支逻辑是否正确。决策点3和4用单元测试参数校验和结果处理都是纯函数容易测。决策点5和6用集成测试模拟多轮循环验证终止条件和上下文裁剪是否按预期工作。决策点7用端到端测试真实调用 LLM验证求助话术是否合理。这个测试策略的核心思想是把不确定的部分LLM 调用隔离出来把确定的部分分支逻辑、参数校验、结果处理测透。这样即使 LLM 行为有波动核心逻辑的稳定性也有保障。9.3 上线之后要盯哪些指标Agent 上线不是终点是起点。我一般会盯四个指标工具调用成功率、平均循环轮数、上下文溢出率、用户求助触发率。工具调用成功率低于 90% 说明工具描述或参数 schema 有问题。平均循环轮数突然升高说明 LLM 可能陷入了某种循环。上下文溢出率大于 0 说明裁剪策略需要调整。用户求助触发率过高说明 Agent 的能力覆盖不够。这四个指标里我最看重的是平均循环轮数。它像一个体温计能最早反映出 Agent 的健康状况。轮数突然升高往往是某个工具开始返回异常结果或者 LLM 的 prompt 需要更新了。10. 几个我踩过的坑和对应的解法最后分享几个实际项目中踩过的坑都是文档里不会写但真实会遇到的。第一个坑是工具调用的并发问题。当 LLM 在一轮里请求调用多个工具时如果你串行执行延迟会累加。我后来改成并行执行但要注意工具之间是否有依赖关系。没有依赖的工具并行调有依赖的串行调。判断依赖关系这件事我目前还是靠人工标注没找到特别好的自动化方案。第二个坑是 LLM 输出的 tool_call 格式不稳定。同一个模型有时候返回 JSON有时候返回带 markdown 代码块的 JSON有时候还会在 JSON 前后加解释文字。我的解法是在解析层做容错先用正则提取 JSON 部分再尝试解析。如果解析失败把原始输出返回给 LLM 让它重新生成。这个重试逻辑加上之后解析失败率从 5% 降到了 0.1% 以下。第三个坑是上下文里的角色混淆。Agent 循环中会产生 user、assistant、tool 三种角色的消息。如果角色标注错了LLM 会把工具返回的结果当成用户说的话导致后续推理完全跑偏。我的做法是在每次追加消息时严格检查角色字段并且写了一个断言函数在开发环境里做校验。上线前跑一遍全量测试用例确保没有角色错乱的情况。第四个坑是 token 计数的偏差。不同模型对同一段文本的 token 计数不一样同一个模型对中英文混合文本的计数也不一样。我一开始用字符数估算结果线上频繁溢出。后来换成用模型自带的 tokenizer 实际计算问题才解决。这个坑的教训是不要估算要实测。这四个坑有一个共同点它们都不是算法问题而是工程问题。Agent 的难点从来不在模型有多聪明而在这些边边角角的工程细节你有没有处理到位。七个决策点每一个拆开看都不复杂但合在一起就是一个完整的运行时系统。你把每个决策点的边界条件都想清楚Agent 的稳定性自然就上来了。我在实际项目里的体会是Agent 开发最花时间的不是写核心循环而是处理各种异常分支。正常流程可能一百行代码就写完了异常处理要写五百行。但正是这五百行决定了你的 Agent 是能 demo 还是能上线。