1. Agent 工程到底在解决什么问题
1.1 从一个真实困境说起
去年下半年我接手了一个内部工具链的改造项目,核心目标是把几个分散的自动化脚本整合成一个能自主决策、能调用外部工具、能根据反馈调整行为的智能体系统。当时团队里有人提议直接上某个开源框架,有人建议自己从零写调度逻辑,争论了整整两周。最后我们选了一条中间路线:用现成的执行框架做底座,自己实现循环控制层,再用图结构管理任务依赖。这套组合拳打下来,效果比预期好很多,也让我对 Agent 工程的三层架构有了比较具体的认知。
很多人第一次接触 Agent 这个概念时,容易把它和传统的自动化脚本混为一谈。脚本是你写死每一步做什么,Agent 是你告诉它目标,它自己决定怎么做、做几步、什么时候停。这个差别听起来不大,但落到工程实现上,复杂度差了一个数量级。脚本跑挂了你知道去哪一行看,Agent 跑挂了,你可能连它为什么做了那个决策都搞不清楚。
1.2 三层架构的由来
Agent 工程发展到现在,社区里逐渐形成了一种共识:一个能上生产的 Agent 系统,基本都可以拆成三层来看。最底层是Harness,负责提供执行环境和工具接口;中间层是Loop,负责驱动 Agent 的思考-行动-观察循环;最上层是Graph,负责编排多个 Agent 或复杂任务的依赖关系。
这三层不是某个标准组织规定的,而是从大量实际项目中抽象出来的。你去看任何一个稍微像样的 Agent 项目,不管它用什么语言写、跑在什么平台上,都能找到这三层的影子。区别只在于有些框架把三层揉在一起了,有些框架只提供了其中一层,剩下的要你自己补。
我个人的判断是:如果你要做的是一个 demo,一层就够了;如果你要做的是一个能跑三个月不出大问题的系统,三层缺一不可。
1.3 谁适合看这篇内容
这篇内容主要面向已经写过一些 Agent 相关代码、但还没形成完整工程方法论的开发者。如果你还在纠结“Agent 和脚本有什么区别”,建议先动手写一个最简单的 ReAct 循环跑通再说。如果你已经在用某个框架做项目,但总觉得哪里不对劲——比如循环控制不灵活、多 Agent 协作一团乱麻、工具调用经常出莫名其妙的问题——那这篇内容应该能帮你理清思路。
我会尽量少讲抽象概念,多讲实际怎么做、为什么这么做、踩过哪些坑。代码示例以 Python 为主,但思路是跨语言的。涉及具体框架的地方,我会说明选型理由,但不会绑定某个特定产品。
2. Harness 层:Agent 的手和脚
2.1 Harness 到底负责什么
Harness 这个词在英文里有“马具”的意思,引申出来就是“驾驭、控制”的意思。在 Agent 工程里,Harness 层负责的是 Agent 与外部世界交互的所有基础设施。具体来说包括这几块:
- 工具注册与发现:Agent 怎么知道有哪些工具可以用、每个工具接受什么参数、返回什么格式
- 执行沙箱:工具跑在什么环境里,有没有资源限制,出错了怎么隔离
- 权限控制:哪些工具可以调、哪些不能调、调用频率有没有限制
- 结果格式化:工具返回的原始数据怎么转成 Agent 能理解的格式
- 错误处理:工具调用失败了,是重试、降级还是直接终止
你可以把 Harness 理解成 Agent 的操作系统。Agent 本身只负责“想”,Harness 负责让“想”变成“做”。
2.2 工具注册的两种常见做法
工具注册这块,我见过两种主流做法。一种是装饰器注册,就是你在函数上面加个注解,框架自动扫描并注册。这种做法的好处是写起来快,坏处是工具和代码耦合太紧,想动态增删工具比较麻烦。另一种是配置文件注册,工具的定义写在一个独立的配置里,运行时加载。这种做法的好处是灵活,坏处是配置和实现容易不同步。
我现在的做法是混合:核心工具用装饰器注册,保证开发效率;需要动态调整的工具用配置注册,保证灵活性。具体实现上,我会维护一个工具注册表,每个工具至少包含这几个字段:
{ "name": "search_database", "description": "根据关键词搜索内部数据库,返回匹配的记录", "parameters": { "type": "object", "properties": { "query": {"type": "string", "description": "搜索关键词"}, "limit": {"type": "integer", "default": 10} }, "required": ["query"] }, "handler": search_database_impl, "timeout": 30, "retry": 2 }这里有几个细节值得注意。description不是写给人看的,是写给 Agent 看的。Agent 会根据这个描述判断什么时候该用这个工具。所以描述要写得具体、准确,不能含糊。我见过有人把描述写成“搜索数据”,结果 Agent 经常在不需要搜索的时候也去调这个工具。改成“根据用户提供的关键词在内部知识库中查找相关文档,适用于需要事实性信息的场景”之后,误调用率明显下降。
parameters用 JSON Schema 定义,这是目前最通用的做法。好处是 Agent 能直接理解参数结构,坏处是写起来比较啰嗦。如果你的工具参数比较简单,可以考虑用更简洁的格式,但一定要保证 Agent 能正确解析。
2.3 执行沙箱的选型考量
执行沙箱这块,选型主要看你的工具是什么类型。如果工具都是纯计算、不涉及外部资源的,那用进程内执行就够了,加个超时控制就行。如果工具会访问文件系统、网络或者执行用户提供的代码,那就必须上隔离。
我目前用的是容器化沙箱,每个工具调用跑在一个轻量容器里。这样做的好处是隔离彻底,坏处是启动有开销。为了平衡,我做了一个容器池,预先启动一批容器待命,调用时直接分配,用完回收。实测下来,单次调用的额外开销能控制在 50 毫秒以内,对于大多数场景够用了。
如果你的工具调用频率很高,比如每秒几十次,那容器池的方案可能还是太重。可以考虑用 WebAssembly 做隔离,启动开销能降到毫秒级,但工具的实现语言会受限。
2.4 权限控制的最小必要原则
权限控制这块,我的原则是最小必要。每个工具在注册时都要声明它需要什么权限,比如读文件、写文件、访问网络、执行命令。Agent 在调用工具前,Harness 会检查当前上下文有没有对应权限。没有就拒绝,并返回一个明确的错误信息。
这样做有两个好处。一是安全,Agent 不会因为某个 bug 或者恶意输入就去执行危险操作。二是可观测,你能清楚地知道每个 Agent 在什么时间用了什么权限,出了问题好排查。
我见过一些项目为了图省事,给 Agent 开了全权限,结果 Agent 在调试过程中把生产数据库给清了。这种事故一旦发生,修复成本极高。所以权限控制这块,再怎么强调都不为过。
2.5 结果格式化的坑
结果格式化看起来简单,实际上坑很多。工具返回的数据格式五花八门,有 JSON、有 XML、有纯文本、有二进制。Agent 的上下文窗口是有限的,你不能把原始数据一股脑塞进去。
我的做法是分两步。第一步是结构化,把各种格式统一转成 JSON。第二步是摘要化,如果数据量超过阈值,就生成一个摘要,只把关键信息放进上下文,完整数据存在外部,Agent 需要时再通过工具去取。
这里有个经验:摘要的生成最好用规则+模型结合的方式。纯规则摘要容易漏掉重要信息,纯模型摘要又可能引入幻觉。我的做法是先用规则提取结构化字段,再用模型生成自然语言描述,两者一起放进上下文。
3. Loop 层:Agent 的心跳
3.1 循环的本质是什么
Loop 层是 Agent 的核心,它决定了 Agent 怎么思考、怎么行动、什么时候停。最基础的循环就是 ReAct 模式:思考、行动、观察,然后重复。听起来简单,但实际实现时要考虑的问题很多。
首先是循环终止条件。Agent 不能无限循环下去,必须有明确的终止条件。常见的终止条件有:达到最大步数、达到最大 token 消耗、Agent 主动输出终止信号、连续多步没有实质性进展。我一般会同时设置多个条件,任何一个满足就终止。
其次是循环状态管理。每一轮循环都会产生新的信息,这些信息怎么组织、怎么传递给下一轮,直接影响到 Agent 的表现。我的做法是维护一个结构化的状态对象,包含历史动作、观察结果、当前目标、已尝试方案等字段。每轮循环开始时,根据当前状态生成提示词;循环结束时,更新状态对象。
3.2 提示词的组织方式
提示词的组织方式对 Agent 表现影响巨大。我试过几种方案,最后稳定下来的做法是分层组织:
- 系统层:定义 Agent 的角色、能力边界、行为准则。这部分基本不变。
- 任务层:描述当前要完成的任务、可用的工具、输出格式要求。这部分每个任务不同。
- 历史层:记录之前的思考、行动、观察。这部分随循环增长。
- 指令层:当前这一步具体要做什么。这部分每轮不同。
分层的好处是清晰,每部分职责明确,修改起来不会互相影响。坏处是提示词会比较长,消耗更多 token。为了控制长度,我会对历史层做压缩,只保留最近几轮的关键信息,更早的用摘要代替。
3.3 循环控制的几种模式
除了基础的 ReAct 循环,我还用过几种变体:
Plan-and-Execute:先让 Agent 制定一个完整计划,然后按计划逐步执行。这种模式适合任务步骤比较明确、不需要太多动态调整的场景。好处是执行效率高,坏处是计划一旦有误,后面全错。
Reflexion:在每轮循环后加一个反思步骤,让 Agent 评估自己刚才的表现,找出问题并调整策略。这种模式适合需要试错的场景,好处是能自我纠正,坏处是消耗更多 token 和时间。
Tree-of-Thought:在每一步生成多个候选方案,评估后选择最优的继续。这种模式适合需要探索的场景,好处是能找到更优解,坏处是计算开销大。
我现在的做法是根据任务类型动态选择模式。简单任务用基础 ReAct,复杂任务用 Plan-and-Execute,需要探索的任务用 Tree-of-Thought。切换逻辑写在 Loop 层的配置里,不用改代码。
3.4 循环中的错误处理
循环中最常见的问题就是工具调用失败。失败的原因很多:网络超时、参数错误、权限不足、服务不可用。不同的失败要区别对待。
我的处理策略是这样的:
| 错误类型 | 处理策略 | 重试次数 |
|---|---|---|
| 网络超时 | 自动重试 | 3 |
| 参数错误 | 返回错误信息给 Agent,让它修正参数 | 2 |
| 权限不足 | 直接终止,记录日志 | 0 |
| 服务不可用 | 等待后重试,超过阈值则降级 | 2 |
| 未知错误 | 返回错误信息,让 Agent 决定 | 1 |
这里的关键是让 Agent 知道发生了什么。很多框架的错误处理就是把异常吞掉,返回一个空结果,Agent 完全不知道出了问题,继续按错误的前提往下走。正确的做法是把错误信息结构化后返回给 Agent,让它有机会调整。
3.5 循环的可观测性
Loop 层是 Agent 最复杂的部分,也是最需要可观测性的部分。我一般会记录这些指标:
- 每轮循环的耗时
- 每轮循环消耗的 token 数
- 工具调用的成功率和平均耗时
- 循环终止的原因
- Agent 的决策路径
这些数据对于调试和优化至关重要。我见过很多项目,Agent 表现不好,但开发者完全不知道问题出在哪,因为没有足够的日志。我的建议是,从第一天就把可观测性做好,后面会省很多事。
4. Graph 层:Agent 的骨架
4.1 为什么需要 Graph 层
单个 Agent 能做的事情是有限的。当任务复杂到需要多个 Agent 协作,或者需要按特定流程执行时,就需要 Graph 层来编排。
Graph 层的核心是把任务拆成节点,节点之间用边连接,边定义了执行顺序和数据流向。每个节点可以是一个 Agent、一个工具调用、一个条件判断,或者一个子图。
我见过一些项目用简单的线性流程来编排多 Agent,一开始能跑,但很快就遇到瓶颈。因为真实任务很少是线性的,更多是分支、循环、并行。用 Graph 来表达这些结构,比用代码硬编码要清晰得多。
4.2 节点类型的设计
节点类型的设计直接决定了 Graph 层的表达能力。我目前支持这几种节点:
- Agent 节点:执行一个 Agent 循环,输入是上下文,输出是结果
- 工具节点:直接调用一个工具,不经过 Agent
- 条件节点:根据条件选择走哪条分支
- 并行节点:同时执行多个子节点,等待全部完成
- 循环节点:重复执行子节点直到满足条件
- 子图节点:执行一个嵌套的 Graph
这几种节点组合起来,基本能表达所有常见的编排模式。设计节点类型时,我的原则是够用就好,不要一开始就追求大而全。每增加一种节点类型,就增加一份维护成本。
4.3 边与数据流
边定义了节点之间的连接关系,也定义了数据怎么流动。我支持两种边:控制边和数据边。控制边决定执行顺序,数据边决定数据传递。
数据边的设计有个关键问题:数据怎么在节点之间传递。我的做法是维护一个全局状态对象,每个节点执行完后把输出写入状态对象的某个字段,下游节点从状态对象读取需要的字段。这样做的好处是解耦,节点之间不需要直接引用;坏处是状态对象会越来越大,需要定期清理。
另一种做法是显式传递,每个节点声明它需要哪些输入、产生哪些输出,Graph 引擎负责匹配。这种做法更清晰,但实现起来更复杂。我目前用的是混合方案:核心数据用全局状态,临时数据用显式传递。
4.4 条件分支的实现
条件分支是 Graph 层最常用的功能之一。实现上有两种方式:边上的条件和条件节点。
边上的条件就是在边上挂一个判断函数,满足条件才走这条边。这种方式适合简单的二选一分支。条件节点是一个独立的节点,它根据输入决定输出哪个信号,下游节点根据信号决定是否执行。这种方式适合复杂的分支逻辑。
我两种都支持,但推荐用条件节点。因为边上的条件多了之后,Graph 的结构会变得很难理解。用条件节点,分支逻辑集中在一处,清晰得多。
4.5 并行执行的注意事项
并行执行能大幅提升效率,但坑也很多。最常见的问题是共享状态冲突。多个节点同时读写全局状态,很容易出现竞态条件。
我的做法是给状态对象加版本号,每次写入前检查版本号,不匹配就重试。这样做能避免大部分冲突,但会降低并发度。如果对性能要求高,可以考虑用不可变数据结构,每个节点产生新的状态副本,最后合并。
另一个问题是错误传播。并行执行的多个节点,如果其中一个失败了,其他节点怎么办?我的策略是:默认等待所有节点完成,然后统一处理错误。如果某个节点失败且标记为关键节点,则取消其他节点,直接返回错误。
4.6 Graph 的可视化与调试
Graph 层最大的优势之一就是可视化。把 Graph 画出来,一眼就能看出整个流程的结构,哪里是瓶颈、哪里可能出问题,一目了然。
我一般会用两种视图:结构视图和执行视图。结构视图展示 Graph 的静态结构,节点和边的关系。执行视图展示一次具体执行的路径,哪些节点执行了、耗时多少、输出是什么。
调试的时候,执行视图特别有用。你能看到 Agent 实际走了哪条路径,和预期是否一致。如果不一致,是条件判断错了,还是数据传递错了,很快就能定位。
5. 三层架构的协作与边界
5.1 层与层之间的接口
三层架构要跑得顺,接口设计很关键。我的做法是定义清晰的接口协议:
- Harness 对 Loop 暴露统一的工具调用接口,Loop 不需要知道工具具体怎么实现
- Loop 对 Graph 暴露统一的执行接口,Graph 不需要知道 Agent 内部怎么循环
- Graph 对上层应用暴露统一的编排接口,应用不需要知道 Graph 内部怎么调度
这样做的好处是每层可以独立演进。比如我想换一个工具执行引擎,只要接口不变,Loop 和 Graph 都不用改。
5.2 什么时候该跨层
严格分层是理想状态,实际项目中经常需要跨层。比如某个工具调用需要根据 Graph 的上下文来决定参数,这就跨了 Harness 和 Graph 两层。
我的原则是:能不分就不分,必须分就显式分。如果确实需要跨层,就定义一个明确的跨层接口,而不是让上层直接访问下层的内部状态。这样至少保证了可追踪性。
5.3 性能优化的切入点
三层架构的性能优化,切入点各不相同:
- Harness 层:优化工具执行速度,减少序列化开销,用连接池
- Loop 层:优化提示词长度,减少不必要的循环,用缓存
- Graph 层:优化调度算法,提高并行度,减少节点间等待
我一般会先做 profiling,找出瓶颈在哪一层,然后针对性优化。盲目优化往往事倍功半。
6. 生产实践中的常见问题与排查
6.1 Agent 陷入死循环怎么办
死循环是 Agent 最常见的问题之一。表现是 Agent 反复执行同样的动作,或者在不同动作之间来回切换,始终不终止。
排查思路是这样的:先看循环终止条件有没有生效。如果最大步数设得太大,Agent 可能在达到步数前就已经陷入死循环了。然后看 Agent 的决策逻辑,是不是某个工具一直返回错误,导致 Agent 反复重试。最后看提示词,是不是有歧义导致 Agent 理解错了任务。
我的解决方法是加一个进展检测机制。每轮循环后,计算当前状态和上一轮状态的差异。如果连续多轮差异很小,就判定为没有进展,强制终止并返回当前结果。
6.2 工具调用参数错误怎么处理
参数错误通常是因为 Agent 对工具的理解有偏差。可能是工具描述不清楚,可能是参数 schema 太复杂,也可能是 Agent 的推理能力不够。
我的处理方法是错误信息要具体。不要只说“参数错误”,要说“参数 query 是必填的,但你没提供”或者“参数 limit 应该是整数,但你提供了字符串”。Agent 看到具体的错误信息,修正的成功率会高很多。
另外,我会在工具注册时加参数校验,在调用前就发现问题,而不是等到执行时才报错。这样能节省一轮循环。
6.3 多 Agent 协作时的信息丢失
多 Agent 协作时,信息在 Agent 之间传递,很容易丢失或失真。常见的原因是状态对象太大,传递时被截断;或者格式不统一,接收方解析失败。
我的做法是定义标准化的消息格式,所有 Agent 之间的通信都用这个格式。消息包含发送方、接收方、类型、内容、时间戳等字段。内容部分用 JSON,保证结构化。传递时只传必要字段,大块数据存外部,传引用。
6.4 Graph 执行卡住怎么排查
Graph 执行卡住,通常是某个节点在等待永远不会到来的输入。可能的原因有:上游节点没执行、数据边配置错误、条件分支走了死路。
排查时我会先看执行日志,确认最后一个成功执行的节点是哪个。然后检查这个节点的下游节点,看它们的输入条件是否满足。如果条件不满足,再看为什么上游没有产生对应的输出。
预防措施是在 Graph 定义时做静态检查,确保每个节点都有可达的输入路径,没有孤立节点,没有死循环。
6.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| Agent 不终止 | 终止条件未生效 | 检查步数和 token 限制 | 加进展检测,强制终止 |
| 工具调用失败率高 | 参数错误或权限不足 | 看错误日志 | 加参数校验,明确错误信息 |
| 多 Agent 信息不一致 | 状态同步问题 | 检查消息传递日志 | 标准化消息格式,加版本号 |
| Graph 执行卡住 | 节点等待输入 | 看执行路径 | 静态检查,加超时 |
| 输出质量不稳定 | 提示词问题 | 对比不同输入的输出 | 优化提示词,加示例 |
| 性能差 | 某层瓶颈 | Profiling | 针对性优化 |
7. 一些个人体会
这套三层架构我在三个项目里用过,最大的感受是:分层不是为了好看,是为了好改。Agent 工程变化太快了,今天用的模型明天可能就换了,今天流行的框架明天可能就过时了。分层之后,换一层不影响其他层,改起来心里有底。
另一个体会是:不要过度设计。我见过一些项目,一开始就搞了很复杂的 Graph 编排,结果实际用到的功能不到十分之一。我的建议是,先从最简单的 Loop 开始,遇到单 Agent 搞不定的问题再上 Graph,遇到工具管理混乱再上 Harness。按需演进,比一开始就搭个大框架要务实得多。
最后分享一个小技巧:给 Agent 加一个“解释”功能。让它在每次决策后,用自然语言解释一下为什么这么做。这个解释不一定要给用户看,但你自己调试的时候会非常有用。很多时候你看日志看不出问题,但一看 Agent 的解释,立刻就知道它哪里理解错了。