1. 从七个零件到七次抉择:我眼中的 AI Agent 工程实现
这两年“AI Agent”这个词被炒得火热,打开任何一个技术社区,满屏都是 agent 开发、agent 框架、agent 学习路线。但真正动手搭过几个能跑起来的 Agent 之后,我发现一个很尴尬的事实:大部分教程都在教你调 API,却没人讲清楚一个 Agent 从“能跑”到“可靠”之间,到底要跨过多少道坎。我自己是从去年开始系统性地折腾 Agent 的,从最简单的“LLM 加个循环”到后来带工具调用、带记忆、带编排的完整系统,踩过的坑能写满一个笔记本。这篇文章我想把 Agent 的工程实现拆成两个视角来讲:一个是静态的“七要素”——也就是一个 Agent 系统由哪七个核心零件组成;另一个是动态的“七个决策点”——也就是在运行时,工程上必须做出的七次关键抉择。这两个视角合在一起,基本上就能把 Agent 的工程实现讲透。不管你是刚听说 agent 是什么的新手,还是已经在用 LLM 框架搭 agent 项目的老手,我相信都能从中找到一些可以直接抄作业的东西。
先说说为什么我要用“七要素”和“七个决策点”这两个框架。市面上讲 Agent 架构的文章不少,但很多要么太学术,上来就是 POMDP、belief state 那一套,看得人云里雾里;要么太浅,就告诉你“Agent 等于 LLM 加工具加循环”,然后没了。我想要的是一种既能指导实际编码、又能帮助理解本质的拆解方式。七要素解决的是“一个 Agent 系统需要哪些组件”的问题,它偏向静态结构;七个决策点解决的是“这些组件在运行时怎么协同”的问题,它偏向动态流程。两者一静一动,正好覆盖了从设计到落地的完整链路。下面我就按这个思路,把每个部分都掰开揉碎讲一遍。
2. 七要素:一个 Agent 系统的静态解剖
2.1 要素一:LLM 作为推理内核,但不是全部
很多人一提到 Agent,第一反应就是“哦,就是 LLM 嘛”。这话对了一半。LLM 确实是 Agent 的推理内核,它负责理解意图、生成计划、决定下一步动作。但如果你把 Agent 等同于 LLM,那就会掉进一个很大的坑:你会以为换个更强的模型,Agent 就自动变好了。实际情况远不是这样。
我拿一个实际例子来说明。之前我做一个自动整理会议纪要的 Agent,最开始用的是某个中等规模的模型,效果一般,经常漏掉待办事项。我以为是模型不够强,换了一个更大的模型,结果待办事项是抓全了,但它开始自作主张地给每个待办事项编造负责人和截止日期。问题出在哪?出在我只换了 LLM,但没有调整工具调用的约束和输出格式的校验。LLM 在 Agent 里的角色更像是一个“大脑”,但大脑再聪明,如果没有合适的“手”和“眼睛”配合,照样干不好活。
所以在工程上,我把 LLM 这个要素进一步拆成三个可配置的维度:模型选型、推理模式、输出约束。模型选型不用多说,根据任务复杂度和成本预算来定。推理模式指的是用不用思维链、用不用自洽性采样、要不要做多步推理。输出约束则是通过 prompt 工程或者结构化输出(比如 JSON schema)来限制 LLM 的输出空间。这三个维度组合起来,才构成一个完整的“推理内核”要素。
提示:不要迷信“最强模型”。在很多 Agent 场景里,一个中等模型加上好的输出约束,效果比一个强模型加上松散的 prompt 要稳定得多。我实测下来,约束带来的收益往往比模型升级更大。
2.2 要素二:工具调用接口,Agent 的手和脚
工具调用是 Agent 区别于普通聊天机器人的核心能力。没有工具调用,Agent 就只能“说”,不能“做”。但工具调用的工程实现里,有很多细节是教程里不会讲的。
首先是工具的粒度问题。工具太粗,比如一个“处理文件”的工具,LLM 很难准确调用,因为它不知道这个工具到底能处理什么格式、返回什么结果。工具太细,比如“读取文件第一行”“读取文件第二行”分开,那工具数量会爆炸,LLM 的选择困难症就犯了。我的经验是,一个工具只做一件事,但这件事要有明确的输入输出契约。比如“读取文件”就是一个好工具,“处理文件”就是一个坏工具。
其次是工具的描述。很多人写工具描述就一句话“读取文件”,然后抱怨 LLM 不会用。工具描述其实是给 LLM 看的“说明书”,你得写清楚:这个工具干什么、什么时候用、输入参数是什么格式、返回什么、有什么限制。我通常会写三段式描述:第一段一句话概括功能,第二段详细说明参数和返回值,第三段给一两个调用示例。这样 LLM 的调用准确率会明显提升。
最后是工具的错误处理。工具调用失败是常态,网络超时、文件不存在、权限不足,各种情况都有。如果工具直接抛异常,整个 Agent 循环就断了。我的做法是让工具永远返回一个结构化的结果,包含success、data、error三个字段,这样 LLM 可以根据错误信息决定是重试、换工具还是放弃。
2.3 要素三:记忆系统,不只是存聊天记录
Agent 的记忆系统经常被简化成“把历史对话塞进 context”。这在简单场景下能用,但一旦任务变长、变复杂,就会出问题。Context 窗口是有限的,你不可能把所有历史都塞进去。而且很多历史信息是冗余的,塞进去反而干扰 LLM 的判断。
我把 Agent 的记忆分成三层:短期记忆、长期记忆、工作记忆。短期记忆就是最近几轮对话,直接放在 context 里。长期记忆是跨会话的持久化信息,比如用户的偏好、之前任务的结论,通常存在向量数据库或者关系数据库里,需要的时候检索出来。工作记忆是当前任务的中间状态,比如已经完成了哪些步骤、当前在哪个环节,这个可以用一个结构化的状态对象来维护。
这三层记忆的读写时机很关键。短期记忆每轮都更新,长期记忆在任务结束时写入、在任务开始时检索,工作记忆在每个决策点更新。我见过很多 Agent 项目只做了短期记忆,结果就是 Agent 聊着聊着就“失忆”了,或者把之前已经确认过的信息又问一遍。加上长期记忆和工作记忆之后,体验会好很多。
2.4 要素四:规划与分解模块,把大任务拆小
规划模块是 Agent 的“项目经理”。用户给一个模糊的大目标,比如“帮我调研一下 AI Agent 的主流架构”,规划模块要把它拆成可执行的子任务:先搜索资料,再筛选重点,然后整理成文,最后检查完整性。
规划的实现方式有好几种。最简单的是让 LLM 一次性生成一个任务列表,然后按顺序执行。这种方式适合步骤明确、依赖关系简单的任务。复杂一点的是动态规划,每执行完一步,根据结果决定下一步做什么。这种方式更灵活,但也更容易跑偏。我通常的做法是混合式:先让 LLM 生成一个粗粒度的计划,然后在执行过程中允许动态调整。
规划模块的一个常见坑是“过度规划”。LLM 有时候会生成一个十几步的计划,其中很多步骤是冗余的或者不可执行的。我的经验是,计划不要超过七步,超过七步就说明任务拆得太细了,应该合并。另外,每个步骤都要有明确的完成标准,否则 Agent 不知道自己什么时候该进入下一步。
2.5 要素五:执行引擎,循环机制的核心
执行引擎是 Agent 的“心脏”,它负责驱动整个循环:调用 LLM 推理、解析输出、执行工具、更新状态、判断是否继续。这个循环机制看起来简单,但工程上有几个关键决策。
第一个决策是循环的终止条件。常见的有:LLM 输出最终答案、达到最大步数、超时、遇到不可恢复的错误。我一般会同时设置最大步数和超时,防止 Agent 陷入死循环。最大步数根据任务复杂度来定,简单任务五到十步,复杂任务可以到二十步。
第二个决策是循环的节奏控制。是每一步都调用 LLM,还是某些步骤可以批量执行?我的做法是,涉及决策的步骤必须调用 LLM,纯执行类的步骤(比如连续读取多个文件)可以批量做。这样既保证了灵活性,又控制了成本。
第三个决策是异常处理。循环中任何一步都可能失败,LLM 调用超时、工具返回错误、输出格式不对。我的做法是给每一步都包一层重试逻辑,重试次数用尽后,把错误信息作为观察结果反馈给 LLM,让它决定下一步怎么办。这样 Agent 就有了一定的“自主容错”能力。
2.6 要素六:状态管理,让 Agent 知道自己在哪
状态管理是很多 Agent 项目容易忽略的部分。没有状态管理,Agent 就不知道自己执行到哪一步了、哪些信息已经收集了、哪些还没做。状态管理做得好,Agent 的行为就会很连贯;做得不好,Agent 就会反复问同样的问题、重复执行同样的步骤。
我用的是一个结构化的状态对象,包含几个关键字段:task(当前任务描述)、plan(计划步骤列表)、current_step(当前步骤索引)、observations(已收集的观察结果)、artifacts(已生成的产物)。这个状态对象在每个循环迭代中更新,并且会作为 context 的一部分传给 LLM。
状态管理的一个难点是状态的序列化和反序列化。如果 Agent 需要支持中断恢复,状态就必须能持久化。我一般用 JSON 来序列化状态,存到数据库或者文件里。这样即使服务重启,Agent 也能从上次中断的地方继续。
2.7 要素七:安全与护栏,别让 Agent 闯祸
安全与护栏是 Agent 从“玩具”走向“工具”的关键。一个没有护栏的 Agent,可能会删除重要文件、发送不当消息、泄露敏感信息。护栏要做在多个层面。
输入层面,要对用户输入做过滤和校验,防止 prompt 注入。输出层面,要对 LLM 的输出做检查,防止生成不当内容。工具调用层面,要对敏感操作做权限控制,比如删除文件、发送邮件这类操作,要么需要二次确认,要么直接禁止。循环层面,要设置资源上限,防止 Agent 消耗过多 token 或调用过多工具。
我自己的做法是维护一个“危险操作清单”,清单上的工具调用必须经过人工确认或者额外的权限校验。这个清单不是固定的,根据具体应用场景来定。比如一个只读的调研 Agent,所有写操作都应该被禁止;一个自动发消息的 Agent,发送操作就需要确认。
3. 七个决策点:Agent 运行时的动态抉择
3.1 决策点一:何时调用 LLM,何时走规则
Agent 循环里最频繁的决策就是“这一步要不要调用 LLM”。全部走 LLM,成本高、延迟大;全部走规则,灵活性差、覆盖不了复杂情况。我的经验是,能用规则的地方就用规则,规则覆盖不了的地方才用 LLM。
具体来说,工具调用的参数解析可以用规则,比如从 LLM 输出里提取 JSON 就是纯规则。工具的选择可以用 LLM,因为需要理解语义。结果的判断可以用规则加 LLM 混合,比如先检查返回码,返回码正常再让 LLM 判断内容是否满足要求。这样搭配下来,成本和效果能取得一个不错的平衡。
3.2 决策点二:工具调用的粒度与编排
前面讲工具要素的时候提到了粒度问题,这里从决策角度再展开一下。在运行时,Agent 面临的一个实际决策是:多个工具调用是串行还是并行?串行简单但慢,并行快但复杂。
我的判断标准是看工具之间有没有依赖。没有依赖的工具可以并行,比如同时搜索三个不同的关键词。有依赖的工具必须串行,比如先读取文件再修改文件。并行调用的时候要注意结果合并的顺序问题,我一般会给每个并行调用分配一个 ID,结果回来之后按 ID 合并。
还有一个决策是工具调用的重试策略。不是所有失败都值得重试。网络超时可以重试,参数错误重试也没用,应该把错误信息反馈给 LLM 让它修正参数。我的做法是给每个工具定义一个retryable属性,只有标记为可重试的错误才走重试逻辑。
3.3 决策点三:记忆的读写时机与内容筛选
记忆系统的决策点在于“什么时候读、什么时候写、读什么、写什么”。读的时机一般是任务开始时和每个决策点。写的时机一般是任务结束时和关键信息产生时。
内容筛选是个技术活。不是所有信息都值得写入长期记忆。我的筛选标准是:跨会话有用的信息才写入长期记忆。比如用户的偏好、项目的背景、之前任务的结论。一次性的中间结果不写入长期记忆,只放在工作记忆里。
检索的时候也有讲究。我一般用向量检索加关键词检索的混合方式,向量检索负责语义相似,关键词检索负责精确匹配。检索结果不要全部塞进 context,按相关度排序取前几条就行。
3.4 决策点四:规划的执行与动态调整
规划在执行过程中经常需要调整。用户的需求可能变化,执行的结果可能不符合预期,外部环境可能改变。Agent 需要有能力在运行时修改计划。
我的做法是设置一个“重新规划”的触发条件。当连续两步没有进展、或者工具返回了预期之外的结果、或者用户输入了新的指令时,触发重新规划。重新规划不是从头再来,而是基于当前状态和已有观察,生成一个新的计划。
重新规划的一个坑是“规划震荡”,就是 Agent 反复修改计划但始终不执行。为了避免这个问题,我会限制重新规划的次数的上限,比如最多三次。超过三次就强制按当前计划执行,或者向用户求助。
3.5 决策点五:错误处理与自主容错
错误处理是 Agent 可靠性的关键。我前面提到过,Agent 的自主容错能力来自于“把错误当作观察结果反馈给 LLM”。但这里有个决策:哪些错误反馈给 LLM,哪些直接终止?
我的分类是:可恢复错误反馈给 LLM,不可恢复错误直接终止。可恢复错误包括工具调用失败、输出格式不对、信息不足。不可恢复错误包括权限不足、资源耗尽、安全违规。对于可恢复错误,LLM 可以根据错误信息决定重试、换工具还是调整计划。对于不可恢复错误,直接终止并报告给用户。
还有一个决策是错误的重试次数。我一般设置每个步骤最多重试两次,整个任务最多重试五次。超过上限就终止,避免无限循环。
3.6 决策点六:输出格式与结构化约束
LLM 的输出格式直接决定了 Agent 循环能不能顺利解析。如果 LLM 输出的是自由文本,解析起来就很麻烦;如果输出的是结构化格式,解析就简单很多。
我的做法是强制 LLM 输出 JSON,并且用 JSON schema 做校验。JSON 里包含几个固定字段:thought(推理过程)、action(下一步动作)、action_input(动作参数)。这样解析逻辑就很统一,不管 LLM 输出什么内容,我都能提取出这三个字段。
结构化输出的一个坑是 LLM 有时候会输出不合法的 JSON,比如多了一个逗号、少了一个引号。我的做法是在解析失败时,用规则做一次修复,修复不了再让 LLM 重新输出。重新输出的 prompt 里会带上解析错误的信息,帮助 LLM 修正。
3.7 决策点七:终止条件与结果交付
终止条件的决策看似简单,其实有很多细节。什么时候算任务完成?是 LLM 说完成了就算完成,还是需要额外的校验?
我的做法是双重校验:LLM 输出最终答案后,再用一个轻量的校验逻辑检查答案是否满足任务要求。比如任务要求“列出三个要点”,校验逻辑就检查答案里有没有三个要点。校验通过才终止,不通过就把校验结果反馈给 LLM 让它补充。
结果交付的格式也要提前定义好。是纯文本、Markdown、还是 JSON?我一般根据下游消费方来定。如果是给人看的,用 Markdown;如果是给程序消费的,用 JSON。交付之前还要做一次清理,去掉中间过程的冗余信息,只保留最终结果。
4. 工程实现中的关键细节与实操要点
4.1 循环机制的具体实现
Agent 的循环机制用伪代码表示大概是这样:
def agent_loop(task, max_steps=10, timeout=300): state = init_state(task) start_time = time.time() for step in range(max_steps): if time.time() - start_time > timeout: return build_timeout_result(state) prompt = build_prompt(state) llm_output = call_llm(prompt) parsed = parse_output(llm_output) if parsed.action == "final_answer": if validate_answer(parsed.action_input, task): return build_result(parsed.action_input) else: state.observations.append("答案校验不通过,请补充") continue tool_result = execute_tool(parsed.action, parsed.action_input) state.observations.append(tool_result) state.current_step += 1 return build_max_steps_result(state)这段代码看起来简单,但每个函数里面都有很多细节。比如build_prompt要把任务描述、计划、历史观察、当前状态都拼进去,还要控制总长度不超过 context 窗口。parse_output要做 JSON 解析和容错。execute_tool要做参数校验、权限检查、错误捕获。
我实测下来,这个循环在大多数任务上都能跑通。关键是要把每个环节的边界情况处理好,比如 LLM 输出为空、工具返回超时、状态更新失败等等。
4.2 工具调用的参数校验与错误恢复
工具调用的参数校验经常被忽略。LLM 生成的参数不一定符合工具的要求,比如该传整数的传了字符串,该传数组的传了单个值。如果不做校验,工具执行时就会报错。
我的做法是在工具定义里加上参数 schema,用 JSON schema 来描述每个参数的类型、格式、取值范围。调用工具之前先用 schema 校验一遍,不通过就把校验错误反馈给 LLM 让它重新生成参数。
错误恢复的策略我分三级:第一级是参数修正,LLM 根据错误信息重新生成参数;第二级是工具替换,如果某个工具反复失败,尝试用功能相似的工具替代;第三级是计划调整,如果工具替换也不行,就重新规划这一步。
4.3 记忆系统的存储与检索方案
记忆系统的存储我一般用两种方案:轻量级用 SQLite,重量级用 PostgreSQL 加 pgvector。SQLite 适合单机部署、数据量小的场景,pgvector 适合需要向量检索、数据量大的场景。
检索的时候,我一般用混合检索:先用关键词检索召回一批候选,再用向量检索排序。关键词检索用全文索引,向量检索用 embedding 相似度。两路结果合并后取 top-k。k 一般取 3 到 5,太多会干扰 LLM,太少可能漏掉关键信息。
写入的时候要注意去重。同样的信息不要重复写入,否则检索结果里会出现大量重复内容。我的做法是写入前先做一次相似度检查,相似度超过阈值的就不写入。
4.4 安全护栏的落地方式
安全护栏的落地我分三层:输入过滤、输出检查、操作审批。
输入过滤主要是防 prompt 注入。我会检查用户输入里有没有试图覆盖系统指令的内容,比如“忽略之前的指令”这类。检测到就拒绝或者转义。
输出检查主要是防不当内容。我会用一个轻量的分类模型或者规则引擎检查 LLM 的输出,发现不当内容就拦截并重新生成。
操作审批主要是针对敏感工具。我会维护一个敏感操作清单,清单上的操作需要二次确认。确认方式可以是人工确认,也可以是额外的权限校验。
5. 常见问题与排查技巧实录
5.1 Agent 陷入死循环怎么办
死循环是 Agent 最常见的问题之一。表现是 Agent 反复执行同样的步骤,或者反复调用同样的工具。原因通常有几个:终止条件没设好、状态没有正确更新、LLM 的输出格式不稳定。
排查的时候先看状态有没有更新。如果状态没更新,Agent 就不知道自己已经执行过某一步,自然会重复。再看终止条件,最大步数和超时都要设。最后看 LLM 输出,如果输出格式不稳定,解析逻辑就会出错,导致循环卡住。
解决死循环的根本方法是加“进展检测”。每一步执行后,检查状态有没有实质性变化。如果没有变化,就触发重新规划或者终止。
5.2 工具调用准确率低怎么提升
工具调用准确率低通常有三个原因:工具描述不清楚、工具数量太多、LLM 能力不够。
提升的方法按优先级排序:第一,优化工具描述,写清楚功能、参数、返回值、示例;第二,合并相似工具,减少工具数量;第三,如果前两步都做了还不行,再考虑换更强的模型。
我实测下来,优化工具描述带来的提升最明显。一个写得很好的工具描述,能让准确率从百分之六十提升到百分之九十以上。
5.3 Context 窗口不够用怎么优化
Context 窗口不够用是长任务 Agent 的常见问题。优化方法有几个:压缩历史观察、按需检索记忆、分层管理 context。
压缩历史观察就是把冗长的工具返回结果做摘要,只保留关键信息。按需检索记忆就是不要把所有记忆都塞进 context,只检索当前步骤需要的。分层管理 context 就是把 context 分成固定部分和动态部分,固定部分放系统指令和工具定义,动态部分放当前状态和最近观察。
我的经验是,经过这三层优化,同样的任务需要的 context 能减少一半以上。
5.4 Agent 输出不稳定的排查思路
输出不稳定表现为同样的输入有时候结果好有时候结果差。原因可能是 LLM 的随机性、prompt 的歧义、状态的不一致。
排查的时候先把 temperature 调到零,排除随机性。如果还不行,检查 prompt 有没有歧义,把模糊的表述改明确。最后检查状态管理,确保每次运行的初始状态是一致的。
如果这些都做了还不稳定,那可能是任务本身太复杂,需要拆成更小的子任务。
| 常见问题 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| 死循环 | 终止条件缺失、状态未更新 | 检查状态变化、检查终止条件 | 加进展检测、设最大步数 |
| 工具调用不准 | 描述不清、工具太多 | 检查工具描述、统计工具数量 | 优化描述、合并工具 |
| Context 不够 | 历史太长、记忆太多 | 统计 token 消耗 | 压缩观察、按需检索 |
| 输出不稳定 | 随机性、prompt 歧义 | 调 temperature、检查 prompt | 明确 prompt、固定状态 |
5.5 实操避坑清单
最后分享几个我踩过的坑,都是文档里不会写的:
- 不要在 prompt 里放太多工具定义,超过十个工具 LLM 的选择准确率会明显下降。如果工具确实多,先做一轮工具筛选,只把相关的工具放进 prompt。
- 工具返回结果不要直接塞进 context,先做一次摘要。原始结果可能很长,摘要后能省很多 token。
- 状态对象不要太大,只存必要信息。状态太大不仅占 context,还容易在序列化时出错。
- 重试逻辑要加退避,不要立即重试。立即重试容易触发限流,退避重试成功率高很多。
- 测试的时候要覆盖边界情况,比如空输入、超长输入、工具全部失败。这些情况在真实环境里都会遇到。
6. 从能跑到可靠:我的 Agent 工程化体会
搭一个能跑的 Agent 不难,难的是让它可靠。我自己的体会是,可靠性来自于对细节的把控。七要素和七个决策点这两个框架,本质上就是帮我把细节梳理清楚。七要素让我知道系统里有哪些零件,七个决策点让我知道运行时要做哪些选择。每个零件、每个选择都有坑,把这些坑填上,Agent 的可靠性就上来了。
还有一个体会是,不要追求一步到位。我一开始就想搭一个全能的 Agent,结果什么都做不好。后来改成先做一个最小可用的版本,只保留最核心的要素和决策点,跑通之后再逐步加记忆、加规划、加护栏。这样迭代下来,每一步都有反馈,改起来也有的放矢。
最后说一个我觉得很重要的点:Agent 的工程实现里,LLM 只是其中一环,而且往往不是最难的那一环。难的是工具的设计、状态的维护、错误的处理、安全的保障。这些“脏活累活”才是决定 Agent 能不能真正用起来的关键。如果你正在搭 Agent,不妨把更多精力放在这些地方,而不是一味地追新模型、新框架。