1. 从七个零件到七个岔路口:我理解的 Agent 工程全貌
AI Agent 这个词在过去一年里被反复咀嚼,几乎每个技术群都在聊。但真正动手搭过几个能跑起来的 Agent 之后,我发现一个挺尴尬的事实:大部分教程都在教你调 API,却很少有人把 Agent 拆开给你看它到底由哪些零件组成,以及每个零件在工程落地时会逼你做哪些决策。这篇东西就是我自己踩坑之后的一次系统复盘,围绕“七要素”和“七个决策点”两条线展开,把 Agent 从概念到能跑起来的工程实现讲透。
先说清楚这篇适合谁看。如果你已经用过大模型,知道什么是 prompt、什么是 token,但一想到要把 LLM 变成一个能自己调工具、自己循环、自己判断下一步的 Agent 就有点发怵,那这篇就是写给你的。如果你已经在用 LangGraph、Spring AI 或者扣子这类平台搭过 Agent,但总觉得“能跑”和“跑得好”之间隔着一层窗户纸,这篇也能帮你把那层纸捅破。核心关键词就几个:AI Agent、LLM、工具调用、循环机制、Agent 框架。我会尽量用大白话,把每个决策点背后的“为什么”讲清楚,而不是甩一堆术语让你自己悟。
我个人的判断是,Agent 的工程实现本质上不是“让模型更聪明”,而是“让模型在一个受控的循环里做有限的事”。这个认知转变很关键。很多人一开始把 Agent 想成一个全知全能的数字员工,结果搭出来的东西要么死循环烧钱,要么工具调用乱成一锅粥。真正能上线的 Agent,往往是那些把边界划得很清楚的。所以下面我会先拆七要素,再讲七个决策点,最后落到实操和排查上。
2. 解构 AI Agent 的七个核心要素
2.1 要素一:LLM 作为推理内核,但它不是全部
LLM 是 Agent 的大脑,这点没人否认。但很多人误以为“换个更强的模型,Agent 就变强了”,这是个典型的认知陷阱。LLM 在 Agent 里承担的是推理和决策,具体来说就是:理解用户意图、决定要不要调工具、调哪个工具、怎么解析工具返回、什么时候停下来。它不负责记忆存储,不负责工具执行,也不负责循环控制——这些是其他要素的活。
我实测下来,选模型的时候不要只盯着榜单。Open LLM Leaderboard 这类公开榜单上的分数,和它在 Agent 场景下的表现往往对不上。原因很简单,Agent 需要的是稳定的结构化输出能力和多轮工具调用的连贯性,而不是单轮问答的准确率。我试过用某个榜单排名很高的模型做工具调用,结果它经常在 JSON 格式上翻车,输出的参数缺个引号,整个循环就断了。后来换了一个榜单排名中等但指令遵循更稳的模型,反而跑得顺。
提示:选 Agent 用的 LLM,优先看它在 function calling 或 tool use 上的专项表现,而不是通用榜单排名。很多模型厂商会单独发工具调用能力的评测,那个更有参考价值。
还有一个容易被忽略的点:LLM 的 token 消耗在 Agent 场景下是成倍放大的。因为每一轮循环都要把历史对话、工具定义、工具返回结果全部塞进上下文。一个简单的任务跑五六轮,token 消耗可能是单轮问答的十几倍。所以选模型时,成本和上下文窗口要一起算,不能只看能力。
2.2 要素二:记忆系统,短期和长期要分开设计
Agent 的记忆分两种,短期记忆和长期记忆,这俩的工程实现完全不是一回事。短期记忆就是当前会话的上下文,通常直接放在 prompt 里,或者用一个消息列表维护。长期记忆则是跨会话的,需要落到外部存储,比如向量数据库或者结构化数据库。
我见过很多新手把两者混在一起,结果就是上下文越来越长,token 爆炸,而且模型开始“遗忘”早期的重要信息。正确的做法是:短期记忆用滑动窗口加摘要的方式控制长度,长期记忆用检索的方式按需注入。具体来说,当对话轮次超过一定数量,就把早期的对话压缩成一段摘要,只保留关键信息,这样既省 token 又不丢上下文。
长期记忆的写入时机也很讲究。不是每句话都值得记,通常只在任务完成、用户明确表达偏好、或者出现重要事实时才写入。我一般会设一个“记忆写入”的工具,让 Agent 自己判断要不要调用。这样比无脑全存要干净得多。另外,记忆的检索策略直接影响 Agent 的表现,简单的向量相似度检索有时候会召回不相关的内容,加一层重排序会好很多。
2.3 要素三:工具调用,Agent 的手和脚
工具调用是 Agent 区别于普通聊天机器人的核心能力。没有工具调用,Agent 就只是个会说话的模型;有了工具调用,它才能查天气、发消息、读写文件、调 API。工具的本质是一段有明确输入输出的函数,Agent 通过 LLM 生成符合格式的调用请求,然后由外部执行器去跑这个函数,再把结果返回给 LLM。
这里有个关键设计:工具的描述(description)和参数 schema 直接决定了 LLM 能不能正确调用。我踩过的坑是,工具描述写得太简略,模型根本不知道什么时候该用它。比如一个“搜索”工具,如果只写“搜索信息”,模型可能在任何需要信息的时候都调它,包括它自己已经知道答案的时候。后来我把描述改成“当需要获取实时信息或你不确定的事实性信息时使用”,调用准确率明显提升。
工具的数量也要控制。我试过给 Agent 挂二十多个工具,结果模型选择困难,经常调错。后来精简到七八个核心工具,每个工具的职责边界清晰,表现就好很多。如果确实需要很多工具,可以考虑分组或者用路由的方式,先让模型选类别,再选具体工具。
2.4 要素四:循环机制,Agent 的心跳
循环机制是 Agent 的发动机。没有循环,Agent 就是一次性的问答;有了循环,它才能“思考-行动-观察-再思考”这样迭代下去。这个循环通常叫 ReAct 循环,核心就是让 LLM 输出一个动作,执行后把结果喂回去,再让 LLM 决定下一步。
循环的终止条件设计是个大坑。最常见的是让 LLM 自己判断“任务完成”,输出一个特定的结束标记。但模型有时候会“自信地”认为任务完成了,实际上还差得远。我的做法是加双重保险:一是 LLM 的结束标记,二是最大循环次数限制。超过次数就强制终止,避免死循环烧钱。这个最大次数我一般设 10 到 15 轮,具体看任务复杂度。
还有一个细节是循环中的错误处理。工具调用失败是常态,网络超时、参数错误、权限不足都可能发生。如果直接把错误信息原样返回给 LLM,它有时候会反复重试同一个错误调用。更好的做法是把错误信息结构化,告诉它“这个调用因为什么原因失败了,建议换一种方式”,引导它调整策略。
2.5 要素五:规划能力,让 Agent 有全局观
规划能力决定了 Agent 是“走一步看一步”还是“先想清楚再动手”。简单的任务不需要显式规划,但复杂任务如果没有规划,Agent 很容易在细节里迷路。规划的实现方式有几种:一种是让 LLM 先输出一个任务分解列表,然后按顺序执行;另一种是用专门的规划模型或者规划 prompt 来生成步骤。
我个人的经验是,规划不要太细。让 LLM 把任务拆成三到五个大步骤就够了,每个步骤内部再让循环机制去处理细节。拆得太细反而容易僵化,因为实际执行中经常需要根据中间结果调整。另外,规划不是一次性的,执行过程中如果发现原计划行不通,要允许重新规划。这个“重新规划”的触发条件可以设成连续两次工具调用失败,或者 LLM 主动判断需要调整。
2.6 要素六:执行环境,Agent 的沙盒
执行环境是 Agent 实际干活的地方。工具调用最终要落到某个环境里执行,可能是本地进程,可能是容器,也可能是远程服务。这里的安全考量很重要,尤其是当 Agent 能执行代码或者操作文件系统的时候。我一般会把执行环境放在容器里,限制它的网络访问和文件系统权限,避免它误操作或者被恶意输入利用。
Agent 安全这块最近讨论很多,像 AgentPoison 这类研究就是在讲怎么通过污染记忆或知识库来攻击 Agent。工程上的应对就是最小权限原则:Agent 能做的事严格限制在它必须做的范围内。比如一个只负责查数据的 Agent,就不要给它写文件的权限。
2.7 要素七:编排层,把前面六个串起来
编排层是 Agent 的骨架,负责把 LLM、记忆、工具、循环、规划、执行环境协调起来。这一层决定了整个系统的架构。现在主流的编排方式有几种:一种是代码优先的,比如 LangGraph,用图的方式定义节点和边,控制流很清晰;另一种是配置优先的,比如扣子这类平台,拖拽配置就能搭起来;还有一种是框架内置的,比如 Spring AI Agent,跟现有技术栈集成方便。
选哪种编排方式,取决于你的团队和技术栈。如果团队是 Python 背景,LangGraph 这类代码优先的框架灵活度高;如果是 Java 团队,Spring AI 可能更顺手;如果想快速验证想法,低代码平台上手最快。没有绝对的好坏,只有适不适合。
3. 七个决策点:工程落地时你必须做的选择
3.1 决策点一:自研还是用框架
这是第一个岔路口。自研的好处是可控,每一行代码你都知道在干什么,出问题好排查。坏处是工作量大,尤其是循环控制、工具调用解析、错误处理这些脏活累活都要自己写。用框架的好处是快,LangGraph、Spring AI 这些框架已经把常见模式封装好了,你只需要关注业务逻辑。
我的建议是,如果你只是想快速验证一个想法,直接用框架。如果你要做的东西对稳定性、性能有要求,或者框架的抽象跟你的需求不匹配,那就自研核心的循环和工具调用部分,其他能用现成的就用现成的。我自己的项目就是混合的:循环控制自己写,因为需要精细的错误处理和日志;工具调用的 schema 解析用现成的库,省事。
3.2 决策点二:单 Agent 还是多 Agent
多 Agent 听起来很酷,但实际落地时,大部分场景单 Agent 就够了。多 Agent 的复杂度是指数级上升的,通信、协调、状态同步都是问题。我见过一些项目,本来单 Agent 能搞定的事,硬拆成多 Agent,结果调试成本翻了好几倍。
什么时候真的需要多 Agent?当任务可以清晰拆分成多个独立子任务,且子任务之间需要不同的工具集或不同的角色设定时。比如一个负责调研、一个负责写作、一个负责审核,这种分工明确的场景,多 Agent 有价值。但如果只是任务步骤多,单 Agent 加规划能力完全能应付。
3.3 决策点三:工具调用的粒度怎么定
工具粒度是个需要反复权衡的点。粒度太粗,一个工具干太多事,LLM 不好控制参数;粒度太细,工具数量爆炸,LLM 选择困难。我的经验是,一个工具对应一个明确的动作,参数控制在三到五个以内。如果一个操作需要很多参数,考虑拆成多个步骤,或者用配置文件的方式预设一部分参数。
举个例子,“发送消息”这个工具,如果参数是“平台、接收者、内容、时间、格式”,五个参数已经有点多了。可以拆成“选择平台”“选择接收者”“编辑内容”几个步骤,每个步骤一个工具,这样 LLM 每次只需要关注一个决策。
3.4 决策点四:循环的终止策略
前面提过终止条件,这里展开说。终止策略我一般用三层:第一层是 LLM 主动输出结束标记,这是最理想的;第二层是最大轮次限制,防止死循环;第三层是超时限制,防止某个工具调用卡死。三层叠加,基本能覆盖绝大多数异常情况。
还有一个细节是“无进展检测”。如果连续几轮循环,Agent 的状态没有实质变化,比如反复调用同一个工具得到相同结果,那就应该强制终止。这个检测逻辑需要自己写,框架一般不带。
3.5 决策点五:上下文怎么管理
上下文管理直接关系到成本和效果。全量保留上下文最简单,但 token 消耗大,而且模型在长上下文里容易迷失。我的做法是分层管理:最近的几轮对话完整保留,早期的对话压缩成摘要,工具返回结果只保留关键字段。这样既控制了长度,又保留了必要信息。
具体实现上,我会给消息列表设一个 token 上限,超过就触发压缩。压缩的时候不是简单截断,而是让 LLM 生成一段摘要,把关键决策和结果保留下来。这个过程本身也要消耗 token,所以要控制压缩频率,不能每轮都压。
3.6 决策点六:错误处理和重试策略
Agent 运行中出错是常态,关键是怎么处理。我的原则是:工具层面的错误尽量在工具内部消化,返回结构化的错误信息给 LLM;循环层面的错误要有重试和降级机制。比如工具调用超时,可以重试一次,再失败就返回错误让 LLM 决定下一步。
重试不能无脑重试,要有退避策略。我一般用指数退避,第一次等一秒,第二次等两秒,第三次等四秒。同时要区分错误类型,参数错误重试没用,网络错误重试有意义。这个判断逻辑可以写在工具执行器里。
3.7 决策点七:可观测性怎么建
Agent 是个黑盒,如果不加日志和追踪,出了问题根本不知道哪一步错了。可观测性我一般从三个层面建:第一层是完整的对话日志,每一轮的输入输出都记下来;第二层是工具调用的详细记录,包括参数、返回、耗时;第三层是性能指标,比如每轮耗时、token 消耗、工具调用成功率。
这些数据不光用于排查问题,还能用来优化。比如你发现某个工具调用成功率很低,可能是描述没写好;某个任务平均要跑十几轮,可能是规划没做好。没有数据,优化就是瞎猜。
4. 实操:从零搭一个能跑的 Agent
4.1 环境准备和技术选型
我以一个具体的场景来演示:搭一个能查资料、能总结、能写文件的 Agent。技术栈我选 Python + LangGraph + 一个支持工具调用的 LLM。选 LangGraph 是因为它的图结构清晰,循环和分支好控制。LLM 我选一个工具调用能力稳定的,具体哪个就不点名了,你根据自己情况选。
环境准备很简单,装几个包就行:
pip install langgraph langchain-openai如果你用的是其他模型,把 langchain-openai 换成对应的包。环境变量里配好 API key,这个不用多说。
4.2 定义工具和 schema
工具定义我用 LangChain 的 tool 装饰器,这样 schema 自动生成,省事。先定义三个核心工具:搜索、总结、写文件。
from langchain_core.tools import tool @tool def search(query: str) -> str: """当需要获取实时信息或你不确定的事实时使用此工具。输入是一个搜索查询字符串。""" # 实际实现调用搜索 API return f"关于 {query} 的搜索结果..." @tool def summarize(text: str) -> str: """当需要把长文本压缩成要点时使用此工具。输入是要总结的文本。""" return f"总结结果..." @tool def write_file(filename: str, content: str) -> str: """当需要把内容保存到文件时使用此工具。输入是文件名和内容。""" with open(filename, 'w') as f: f.write(content) return f"已写入 {filename}"注意每个工具的 docstring,这就是给 LLM 看的描述。描述里要写清楚“什么时候用”和“输入是什么”,这两点最关键。
4.3 构建循环图
用 LangGraph 构建循环,核心是定义节点和条件边。节点包括:LLM 推理节点、工具执行节点。条件边决定是继续循环还是结束。
from langgraph.graph import StateGraph, END from typing import TypedDict, Annotated import operator class AgentState(TypedDict): messages: Annotated[list, operator.add] def call_llm(state): # 调用 LLM,传入消息和工具定义 response = llm.invoke(state["messages"], tools=tools) return {"messages": [response]} def execute_tools(state): # 执行 LLM 请求的工具调用 last_message = state["messages"][-1] results = [] for tool_call in last_message.tool_calls: result = execute_tool(tool_call) results.append(result) return {"messages": results} def should_continue(state): last_message = state["messages"][-1] if last_message.tool_calls: return "tools" return END graph = StateGraph(AgentState) graph.add_node("llm", call_llm) graph.add_node("tools", execute_tools) graph.set_entry_point("llm") graph.add_conditional_edges("llm", should_continue, {"tools": "tools", END: END}) graph.add_edge("tools", "llm") app = graph.compile()这个图的结构就是:LLM 推理 -> 如果有工具调用就去执行 -> 执行完回到 LLM -> 如果没有工具调用就结束。这就是最基础的 ReAct 循环。
4.4 加入循环控制和错误处理
上面的图有个问题:没有最大轮次限制,可能死循环。加一个计数器:
class AgentState(TypedDict): messages: Annotated[list, operator.add] loop_count: int def should_continue(state): if state["loop_count"] >= 15: return END last_message = state["messages"][-1] if last_message.tool_calls: return "tools" return END工具执行那里也要加错误处理,把异常捕获后返回结构化的错误信息,而不是让异常直接抛出去。
4.5 上下文压缩的实现
当消息列表太长时,触发压缩。我一般设一个阈值,比如消息数超过 20 条就压缩。压缩的逻辑是把最早的一批消息拿出来,让 LLM 生成摘要,然后用摘要替换掉那批消息。
def compress_context(messages): if len(messages) < 20: return messages old_messages = messages[:10] recent_messages = messages[10:] summary = llm.invoke(f"总结以下对话的关键信息:{old_messages}") return [summary] + recent_messages这个压缩在每轮循环开始前检查一次,需要压缩就压。
4.6 跑起来和观察
搭好之后跑一个测试任务,比如“搜索一下最近 AI Agent 的进展,总结成三点,写到 report.txt 里”。观察它的执行过程:第一轮 LLM 应该决定调搜索工具,第二轮拿到搜索结果后决定调总结工具,第三轮决定调写文件工具,第四轮判断任务完成输出结束标记。
如果中间哪一步不对,看日志。日志里应该有每一轮的完整消息、工具调用的参数和返回、耗时。这些信息是排查问题的关键。
5. 常见问题与排查技巧实录
5.1 工具调用格式错误怎么办
这是最常见的问题,表现为 LLM 输出的工具调用 JSON 格式不对,解析失败。原因通常是模型对工具 schema 的理解不够准确,或者 schema 本身太复杂。解决办法有几个:一是简化 schema,减少嵌套层级;二是换一个工具调用能力更强的模型;三是在 prompt 里加 few-shot 示例,给一两个正确的调用样例。
我实测下来,few-shot 示例的效果最明显。在系统 prompt 里放一两个“用户问什么,应该调什么工具,参数怎么填”的例子,格式错误率能降一大半。
5.2 Agent 反复调用同一个工具
这种情况通常是工具返回的结果没有让 LLM 满意,它觉得信息不够,就再调一次。解决办法是让工具返回更明确的信息,或者在返回里加一个“这是最终结果”的标记。另外,可以在循环控制里加检测,如果连续两轮调用同一个工具且参数相同,就强制中断,返回提示让 LLM 换策略。
5.3 任务跑一半就停了
有时候 Agent 会“自信地”认为任务完成了,实际上没完成。这通常是结束判断太宽松。解决办法是在 prompt 里明确任务完成的定义,比如“必须所有子任务都完成且结果已输出才能结束”。另外可以加一个验证步骤,让 LLM 在结束前自查一遍。
5.4 token 消耗太快
token 消耗快一般是上下文太长或者循环轮次太多。先看上下文,是不是把工具返回的完整结果都塞进去了,很多工具返回一大段 JSON,其实只需要其中几个字段。再看循环,是不是规划没做好,导致走了很多弯路。优化这两个地方,token 消耗通常能降一半以上。
5.5 并发场景下的问题
AI Agent 怎么扛并发是个热门话题。单实例的 Agent 在并发请求下,状态管理是个挑战。我的做法是把每个会话的状态独立存储,用会话 ID 隔离。执行器层面用异步或者线程池处理并发请求。如果并发量很大,考虑把 Agent 做成无状态的,状态全部外置到 Redis 这类存储里。
5.6 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决思路 |
|---|---|---|---|
| 工具调用格式错误 | schema 复杂或模型能力不足 | 看原始输出 | 简化 schema,加 few-shot |
| 反复调同一工具 | 工具返回不满足 | 看工具返回内容 | 优化返回,加循环检测 |
| 任务提前结束 | 结束判断太宽松 | 看结束前的消息 | 明确完成定义,加自查 |
| token 消耗快 | 上下文长或轮次多 | 统计 token 分布 | 压缩上下文,优化规划 |
| 并发下状态混乱 | 状态未隔离 | 看会话 ID 管理 | 状态外置,会话隔离 |
6. 一些关于 Agent 工程的个人体会
搭 Agent 这件事,我最大的体会是:不要追求“智能”,要追求“可控”。一个能稳定完成有限任务的 Agent,比一个偶尔惊艳但经常翻车的 Agent 有价值得多。工程上的很多决策,本质上都是在可控性和灵活性之间找平衡。
另一个体会是,Agent 的效果很大程度上取决于工具设计和 prompt 设计,而不是模型本身。同样的模型,工具描述写得好不好,prompt 引导得对不对,效果能差出好几倍。所以与其花时间追新模型,不如把现有模型的工具和 prompt 打磨好。
最后说一个我踩过的坑:不要一开始就追求多 Agent 或者复杂架构。从最简单的单 Agent 加两三个工具开始,跑通了再逐步加复杂度。我见过太多项目,一上来就设计得很复杂,结果连最基本的循环都跑不稳。简单的东西跑稳了,复杂的东西才有基础。