工具、记忆、自主度与多 Agent:生产级系统怎么搭
如果说上篇讲的是 Agent 的"脑子"------规划、推理、反思与执行循环,那么中篇更接近真正落地时最容易出问题的部分:工具、记忆、自主度、多 Agent 协作 。
这四件事看起来都很工程,但实际上决定了 Agent 能不能从"能演示"走到"能上线"。
现实里的 Agent 往往不是"不会想",而是:
- 看不到关键上下文
- 选不到合适工具
- 记不住任务进度
- 长任务执行到一半失去状态
- 多 Agent 协作时彼此放大错误
所以这一篇不谈抽象能力,只谈一件事:系统要怎么设计,才能让 Agent 稳定干活。
一、工具设计:Agent 能做什么,首先由工具边界决定
上下文决定模型能看到什么,工具决定模型能做什么。
这是 Agent 工程里最基础的一条原则。
模型再强,没有合适工具,也只能"想";工具设计不对,Agent 就会在错误接口上反复试错。
1.1 工具不是越多越好,关键是贴不贴近任务
很多团队一开始会把已有 API 全量封装成工具,觉得"覆盖越全越好"。
实际效果往往相反:
- 工具太多,选择成本高
- 工具粒度太细,完成一个目标要拼很多步
- 工具描述太弱,模型不知道什么时候该用
- 工具返回太原始,模型还得自己提炼信息
- 出错只给一句
Error,Agent 不知道怎么修
如果一个工具只描述"它能做什么",却没说明:
- 什么时候用
- 什么时候不用
- 参数应该怎么填
- 出错后怎么改
那它对 Agent 来说通常不够好。
1.2 好工具与差工具的差别
|----|---------------------|--------------------------------------------|
| 维度 | 好工具 | 差工具 |
| 粒度 | 对应 Agent 要完成的目标 | 对应 API 能做的操作 |
| 示例 | update_yuque_post | get_post + update_content + update_title |
| 返回 | 与下一步决策直接相关 | 完整原始数据 |
| 错误 | 结构化,带修正建议 | 通用字符串 "Error" |
| 描述 | 说明适用边界 | 只写功能说明 |
工具不是功能清单,工具是动作接口。
1.3 工具设计如何演进
第一代:API 封装
最早的做法是把 API Endpoint 直接包成工具。
问题是粒度太细,Agent 要自己拼路径、管中间状态、猜调用顺序。
这对人类工程师还行,对 Agent 负担很重。
第二代:ACI,Agent-Computer Interface
ACI 的核心是:工具应该对应 Agent 的目标,而不是底层 API 的边界。
不要给 Agent 一个只表达底层操作的通用接口,比如:
update(id, content)
而是给:
update_yuque_post(post_id, title, content_markdown)
它表达的是一个完整动作,而不是一个零碎接口。
给 Agent 的不是 API,而是可执行意图。
第三代:Advanced Tool Use
到了更成熟的阶段,不只是工具本身,连工具发现、工具调用、工具表达方式都要一起优化。
1)Tool Search:按需发现工具
不要把所有工具一次性塞给模型,而是按任务动态发现。
这样可以显著降低上下文负载,也让模型只在相关工具集合里做选择。
2)Programmatic Tool Calling:代码编排
不要让中间结果一轮轮穿过模型,而是让代码负责执行链路。
模型决定策略,代码负责流水线,中间状态留在运行环境里,不再反复进入上下文。
3)Tool Use Examples:示例驱动
JSON Schema 只能告诉模型参数类型,不能告诉它调用场景。
给工具附上真实示例,往往比纯文字说明更有效。
1.4 设计工具时,先看能不能修回来
工具设计不是看"能不能调",而是看:
调错了以后,Agent 能不能自己修回来。
这也是判断工具质量的重要标准。
三个原则:
- 参数要明确
尤其是 ID、路径、时间窗口、资源名等字段,必须尽量减少歧义。 - 错误要结构化
失败不能只返回一句Error,要告诉 Agent 错在哪里、能否重试、怎么改。 - 定义和实现要绑定
schema、描述、运行逻辑、错误提示最好统一维护,避免文档和实现脱节。
1.5 调试 Agent,先查工具,不要先怪模型
大多数"工具选错了"的问题,根因不在模型,而在工具定义。
优先检查:
- 工具描述是否清楚说明适用边界
- 输入参数是否过宽、歧义是否太大
- 返回值是否包含下一步所需信息
- 错误是否结构化
- 是否存在不该新增的冗余工具
能用 Shell 处理的,不要专门做工具;只是静态知识的,不要做工具。
二、工具消息隔离:框架状态不要直接喂给 LLM
很多 Agent 框架会在运行过程中产生大量内部事件,例如:
- 压缩发生了
- 某个工具调用被跳过了
- 通知队列有新消息
- 某个状态字段被更新了
这些信息对框架很重要,但不一定适合直接进入 LLM。
如果全部喂给模型,只会带来两个问题:
- 模型看到一堆它不需要理解的字段
- token 被白白消耗
2.1 分层消息设计
比较稳妥的做法是区分两类消息:
AgentMessage
给应用层或框架层使用,可以携带任意自定义字段。
Message
真正送入 LLM 的消息,只保留标准类型:
userassistanttool_result
这样做的好处是:
- 会话历史保留完整框架状态
- LLM 输入更干净
- 更容易做压缩、过滤和审计
框架要保留状态,模型只需要看到决策输入。
三、记忆系统:Agent 记不住,不是模型问题,是系统没设计好
Agent 没有原生时间连续性。
会话结束后,上下文清空,下一次启动不会自动记得上次发生了什么。
所以要让 Agent 具备跨会话一致性,记忆层必须单独设计。
记忆不是附加功能,而是基础设施。
3.1 四种记忆,不是四种存储,而是四类问题
1)上下文窗口:工作记忆
当前任务所需的最小信息。容量有限,必须主动管理。
2)Skills:程序性记忆
"怎么做某件事"的固定流程、领域规范、工具使用方式。适合按需加载,不应默认常驻。
3)JSONL 会话历史:情景记忆
记录"发生了什么",支持跨会话检索、回放、审计。
4)MEMORY.md:语义记忆
Agent 主动写入的重要事实、稳定偏好、长期结论。下一次启动时注入系统提示。
3.2 记忆系统的核心不是存储,而是注入控制
真正重要的问题不是"存哪儿",而是:
- 什么时候注入
- 注入多少
- 注入到哪里
- 过期信息怎么处理
- 整合失败怎么回退
成熟的记忆系统,不是把所有历史都留下,而是把最有用的状态,以最小成本带回当前任务。
3.3 Skills 和 MEMORY.md 的分工
- Skills 放稳定流程、操作规范、领域方法
- MEMORY.md 放长期稳定事实、已验证偏好、历史结论
可以简单理解为:
- Skills 是"方法"
- MEMORY.md 是"事实"
方法放 Skill,事实放 Memory,别混。
3.4 记忆整合:只移动指针,不删除原文
记忆分层后,下一步不是"要不要存",而是:
什么时候整合,以及整合失败怎么办。
一个典型流程是:
- 消息流持续增长
tokenUsage / maxTokens >= 0.5- 触发整合
- 对待整合消息执行 summarize
- 成功:写入 MEMORY.md,并移动整合指针
- 失败:原始消息写入 archive/
关键原则只有一个:
系统只移动指针,不删除原始消息。
这样即使摘要失败,也能回到原始存档继续工作。
这比"摘要写得漂亮"重要得多,因为它保证了可回退、可审计。
3.5 记忆实现上,别一开始就过度复杂
很多场景下,记忆系统不需要一上来就做复杂向量库。
结构化 Markdown + 关键词搜索,往往已经足够:
- 可读
- 可改
- 可调试
- 成本低
- 维护简单
只有当规模上来、语义召回确实有收益时,再引入更重的检索方案更合理。
四、逐步放开自主度:先补基础设施,再谈放权
这里说的自主度,不是"少几次人工确认",而是让 Agent 能在更长时间跨度内稳定推进任务。
但前提不是直接放权,而是先补齐三类能力:
- 跨 session 续跑
- 单 session 内进度约束
- 慢速 I/O 的后台接入
4.1 长任务如何跨 session 继续
长任务最常见的失败,不是单步报错,而是 session 结束时任务还没做完。
更稳定的模式,是把长任务拆成两个角色:
Initializer Agent
只运行一次,负责生成:
feature-list.jsoninit.sh- 初始 git commit
claude-progress.txt
作用是把任务外化成可持久化状态。
Coding Agent
后续多个 session 循环执行:
- 从文件恢复现场
- 定位当前任务
- 实现一个功能
- 跑测试
- 更新
passes - 提交代码后退出
进度要放在文件里,不要放在上下文里。
4.2 任务状态要显式写出来
跨 session 解决的是"下次从哪里继续",
单 session 内还要解决"当前做到哪一步"。
一个简单状态结构可以是:
{
"tasks": [
{"id": "1", "desc": "读取现有配置", "status": "completed"},
{"id": "2", "desc": "修改数据库 schema", "status": "in_progress"},
{"id": "3", "desc": "更新 API 接口", "status": "pending"}
]
}
建议约束:
- 同一时间只能有一个
in_progress - 每完成一步,先更新状态,再继续下一步
- 长时间不更新状态时,可注入轻量提醒
这类显式状态管理,本质上是在给 Agent 加外部执行骨架。
4.3 后台 I/O 接入:别让慢操作拖死主循环
当 Agent 自主度提高后,真正拖慢主循环的,通常不是推理,而是文件读写、网络请求和长耗时命令。
更务实的做法是把慢速 subprocess 放到后台线程,通过通知队列在下一轮 LLM 调用前注入结果。
主循环只需要每轮开始前检查是否有新结果,再决定继续执行、等待还是调整计划。
这通常比把整个 loop 改成复杂 async runtime 更稳,也更容易维护。
五、多 Agent 如何组织:先解决隔离,再解决协作
一说到多 Agent,很多人先想到并行。
但工程上首先要解决的,其实不是并行,而是隔离与协作。
5.1 指挥者模式 vs 统筹者模式
指挥者模式
同步协作,人与单个 Agent 紧密互动,每一轮都要人工调整决策。
问题是 session 一结束,context 就没了,产出短暂。
统筹者模式
异步委派,人在开始时设定目标,中间让多个 Agent 并行工作,最后审查产出。
人只在起点和终点出现,中间产出变成可持久化工件。
多 Agent 的价值,不是多开几个模型,而是把人的持续参与变成对工件的最终审核。
5.2 多 Agent 的典型拓扑
常见组织方式是:
- 主 Agent 作为 Orchestrator 统筹全局
- 下挂多个子 Agent 并行工作
- 子 Agent 通过 JSONL inbox 协议通信
- 用 Worktree 隔离文件修改
- 用任务图管理依赖关系
为什么要这样做?
因为多 Agent 协作的核心不是"分工",而是:
- 分隔上下文
- 保留探索历史
- 让结果可追溯
- 防止彼此污染
5.3 子 Agent 适合做什么
子任务里的搜索、试错和调试过程,不应该污染主 Agent 的上下文。
主 Agent 真正需要的是结论,不是过程。
const result = await runAgentLoop(task, { messages: [] });
return summarize(result);
这样做的好处是:
- 主上下文更清爽
- 子任务可大胆试错
- 失败历史不会拖垮主任务
- 并行更自然
5.4 多 Agent 协作必须写成协议
多 Agent 协作一旦靠自然语言对齐,很快就会失控。
模型记不稳谁承诺了什么、谁在等谁、谁已经完成、谁还没开始。
所以必须把协作写成协议,而不是写成聊天。
{
request_id, from_agent, to_agent,
content,
status: 'pending' | 'approved' | 'rejected',
timestamp
}
落地方式通常是:
- 写入:
.team/inbox/{agentId}.jsonl - 特点:append-only,崩溃可恢复
- 读取:按行解析,按
status过滤
5.5 幻觉会互相放大,交叉验证不能省
多 Agent 频繁互动时,错误会层层放大:
- Agent A 先带偏
- Agent B 跟着强化
- Agent C 继续叠加
- 最终所有 Agent 收敛到同一个高置信度错误结论
所以交叉验证很重要。
它的作用不是"再找一个模型复读",而是打断错误共识的形成链路。
5.6 子 Agent 的两个边界
1)深度限制
防止无限递归生成孙 Agent,设一个最大深度即可。
2)最小系统提示
只给必要的三部分:
- Tooling
- Workspace
- Runtime
不要带 Skills 和 Memory 指令。
这样可以避免权限外泄,也避免破坏隔离边界。
结语
中篇讲的不是"模型有多强",而是"系统怎么把模型用对"。
- 工具决定它能做什么
- 记忆决定它能记住什么
- 自主度决定它能做多远
- 多 Agent 决定它能不能扩展规模
生产级 Agent 的核心,从来不是一句"更智能",而是四个更具体的问题:
- 工具是否贴近任务语义
- 记忆是否可控、可回退、可审计
- 自主度是否有明确边界
- 多 Agent 是否有协议、有隔离、有验证
真正决定 Agent 上限的,往往不是"模型能不能想",而是"系统能不能让它持续、稳定、可验证地把事情做完"。