真正的 Agent 工程:给一个不确定的大脑,造一副确定的身体
我以前一直以为,做一个 Agent,核心问题无非是三件事:
给模型接上工具;
让模型记住历史;
再套一层 ReAct,让它一边思考一边行动。
直到我真正拆解 Hermes Agent 的工程架构,才意识到,这种理解只停留在 Demo 阶段。
一个 Agent 只要开始长期运行,只要它能够调用工具、修改文件、连接企业系统,问题就会迅速发生变化。
你面对的不再只是:
模型下一步应该调用哪个工具?
而是:
这个任务执行到哪一步了?
工具到底有没有成功?
用户点击停止以后,已经发出的邮件怎么办?
两个工具同时修改一个文件,谁先发生?
记忆已经写进数据库,为什么模型还是不知道?
对话压缩以后,哪些细节已经永远丢失?
Agent 总结出的 Skill,究竟是经验,还是一次被永久固化的错误?
到了这里,Agent 工程已经不再是“提示词加工具调用”。
它开始变成一个完整的状态系统、任务系统、并发系统和权限系统。
Hermes 最值得学习的地方,正是在这里。
它没有发明大模型,但它把大模型变成长期运行系统以后,真正困难的问题集中暴露了出来。
一、先放弃一个错误直觉:系统知道,不等于模型知道
假设你做了一个企业 Agent。
用户对它说:
以后生成管理层 PPT,全部使用 16:9,不要再使用 4:3。
Agent 调用 Memory 工具,把这条要求写进了数据库。
此时,大部分人的直觉是:
已经保存了,那模型之后当然就知道。
但事实不一定如此。
因为在一个 Agent 系统中,“信息存在”至少有三种完全不同的含义。
第一种:系统已经保存
例如这条要求已经存在于:
数据库
MEMORY.md
用户配置表
会话记录
这意味着即使程序重启,信息仍然存在。
它是系统真正的持久状态。
第二种:模型当前看到了
模型不会自动读取整套数据库。
每次调用模型时,Agent Runtime 都要从大量信息中挑选一部分,放进本轮 Prompt:
系统规则
最近对话
长期记忆
相关 Skill
检索资料
工具结果
只有真正进入本轮上下文的信息,模型才看得到。
数据库里有,不代表当前 Prompt 里有。
第三种:只在当前运行中临时存在
例如:
用户刚刚点击停止
某个工具正在运行
当前 Token 即将超限
一个子 Agent 还没有返回结果
本轮临时检索到了三份文档
这些信息可能只存在于内存中,任务结束后就消失。
所以长期 Agent 中最重要的一句话是:
系统已经保存,不代表模型当前能看到;模型当前看到,也不代表系统已经永久保存。
这不是文字游戏,而是状态一致性的核心。
假设模型刚刚生成了一份完整分析,但程序还没来得及写入数据库就崩溃了。
结果是:
模型生成过
系统没有保存
反过来,Memory 文件已经更新,但当前会话的 System Prompt 没有重新构建。
结果是:
系统已经保存
模型当前仍然看不到
一旦你理解了这个问题,就会发现很多所谓的“模型失忆”,其实根本不是模型能力不足,而是状态没有正确进入工作上下文。
二、真正成熟的 Agent,会把 Prompt 当成“状态快照”
很多人把 Prompt 理解成一段文字。
在长期 Agent 中,更准确的理解是:
Prompt 是模型在某个时间点,对系统状态的一次观察快照。
假设某次会话开始时,模型看到的是:
用户喜欢简洁汇报
当前项目是面料推广
默认使用集团 PPT 模板
会话进行到一半,Agent 又写入了一条新 Memory:
用户要求管理层汇报必须使用 16:9
问题来了:
是否应该马上重新构建整个 System Prompt?
直觉上当然应该。
否则模型不是看不到最新要求吗?
但如果每写一次 Memory,都重建一次 System Prompt,会带来另一个严重问题:Prompt Cache 失效。
现代模型服务为了降低延迟和推理成本,通常会缓存长 Prompt 的固定前缀。
例如一个 Agent 的 System Prompt 很长,里面包含:
身份规则
工具说明
项目规范
Skill 索引
Memory
用户画像
安全限制
只要这些固定前缀保持不变,后续请求就有机会复用之前的计算结果。
但如果你在 Prompt 最前面放了一个不断变化的信息:
当前时间:11:30
下一轮变成:
当前时间:11:31
即使只变化了一分钟,也可能导致后面大段缓存无法继续复用。
于是 Agent Runtime 必须做一个并不完美的选择:
方案一:实时更新 Prompt
优点是模型立即看到最新状态。
缺点是固定前缀频繁变化,缓存命中率下降,延迟和成本上升。
方案二:会话内冻结 Prompt
会话开始时构造一次状态快照,之后尽量保持不变。
新 Memory 虽然已经写入持久化系统,但可能要到下一次会话或者重新加载后,才完整进入 System Prompt。
这很像数据库中的快照隔离。
一次事务开始以后,看到的是某个时间点的数据版本。即使数据库随后发生了变化,当前事务也不一定立即可见。
因此可以把它理解成:
Memory 文件:最新持久状态
当前 System Prompt:会话开始时的状态快照
这也解释了一个重要的 Prompt 工程原则:
高频变化的信息,不应该轻易放在固定 System Prompt 的前部。
当前时间、Token 剩余量、临时搜索结果、任务进度,更适合以本轮临时上下文的形式追加,而不是反复破坏整个稳定前缀。
普通提示词工程关心一句话怎么写。
Agent Prompt 工程还必须关心:
这段内容放在哪里
多久更新一次
是否影响缓存
是否应该进入持久状态
当前会话是否必须立即可见
这已经不是写文案,而是在设计状态生命周期。
三、上下文压缩不是“记忆”,而是一张有损的工作视图
Agent 执行一个复杂任务时,消息会快速膨胀:
用户要求
模型输出
网页搜索结果
文件内容
终端日志
异常堆栈
工具返回
子 Agent 报告
不可能把这些内容永远完整放进上下文。
于是需要压缩。
很多人的理解是:
把前面的聊天总结一下,节省 Token。
这句话没有错,但过于轻描淡写。
更准确的说法是:
从完整历史中,生成一份更小、更适合模型继续工作的有损视图。
注意,是有损。
假设原始对话里有三句话:
用户最初要求使用集团模板
后来要求这次暂时不用集团模板
最后确认从下次开始恢复集团模板
压缩模型可能总结成:
用户对集团模板的使用要求进行过调整。
这句话表面上没有错,但最重要的信息消失了:
现在到底应该用,还是不用?
如果之后再对这段摘要进行第二次摘要,信息还会继续丢失。
它和 JPEG 图片反复压缩很像。
第一次看起来差别不大。
压缩十次以后,边缘、纹理和细节已经严重失真。
因此,正确的 Agent 系统不应该让压缩摘要替代完整历史。
更合理的结构是:
完整历史
↓
永久保存,用于审计、追溯和重新检索
压缩上下文
↓
只作为模型当前继续工作的视图
这很像数据库中的原始表和物化视图。
完整 Session 是原始记录。
压缩摘要是为了提高模型工作效率生成的物化视图。
视图可以重建,可以更新,也可能存在误差,但原始数据不能因为生成了视图就被删除。
这也是为什么以下内容绝不能只依赖对话摘要保存:
订单金额
合同条款
付款状态
审批结论
生产任务状态
用户最终确认
这些信息必须进入结构化业务系统。
上下文负责帮助模型理解任务。
它不应该承担数据库的职责。
四、工具并发最危险的地方:日志顺序不等于真实发生顺序
这是 Agent 工程中最容易被忽略的问题。
假设模型一次生成两个 Tool Call:
1. 修改 config.yaml
2. 读取 config.yaml
为了提高速度,Runtime 把两个工具放进线程池,并发执行。
模型的意图似乎很清楚:
先修改
再读取修改后的结果
但并发系统并不理解模型的语义。
真实执行过程可能是:
读取工具先执行,读到了旧配置
修改工具后执行,完成了修改
系统为了维持工具消息格式,最后仍然可能按照模型原始调用顺序,把结果放回上下文:
Tool 1:修改成功
Tool 2:读取到旧配置
模型看到的日志顺序是:
先修改
后读取
现实世界中的发生顺序却是:
先读取
后修改
这里出现了一个关键区别:
消息的展示顺序,不等于副作用的发生顺序。
在并发系统中,真正重要的是两个操作之间是否存在确定的 happens-before 关系。
也就是:
操作 A 是否被系统保证在操作 B 开始前完成?
如果没有依赖、锁或者事务,答案通常是否定的。
再举一个更直观的例子。
账户原始余额是 500。
模型同时调用:
Tool A:余额增加 100
Tool B:读取余额
模型希望 B 读到 600。
但如果 B 先执行,它读到的就是 500。
即使日志最后把 A 的结果放在 B 前面,也不能改变 B 已经读取旧数据的事实。
这意味着,一个真正成熟的 Tool Runtime,不能只记录:
工具名称
工具描述
输入参数
执行函数
还需要理解工具的副作用属性:
是否只读
是否修改外部状态
影响什么资源
是否可以并行
是否幂等
失败后能否重试
是否需要审批
是否支持补偿
例如,一个文档搜索工具可以声明:
name: search_documents
side_effect: read
parallel_safe: true
idempotent: true
一个订单修改工具则可能声明:
name: update_order
side_effect: write
resource: order:{order_id}
parallel_safe: false
idempotent: false
requires_approval: true
有了这些信息,调度器才有可能做出正确判断:
多个独立搜索工具,可以并发
修改同一订单的工具,必须串行
付款工具,执行前必须审批
非幂等写操作,不能因为超时就盲目重试
很多 Agent 框架把 Tool Call 做成了函数调用。
但一旦工具能够改变现实世界,它就不再只是函数。
它开始接近一个分布式事务节点。
五、用户点击停止,并不意味着现实会自动回滚
假设一个 Agent 正在执行任务。
用户突然发现方向错了,点击停止。
很多产品会显示:
任务已取消
但“取消”到底取消了什么?
至少要分成三个层次。
第一层:停止模型生成
系统停止等待模型 API,或者直接丢弃尚未完成的模型输出。
这一层相对容易。
第二层:停止本地执行
例如 Agent 正在运行 Python、处理文件、执行浏览器自动化。
系统可以尝试终止进程或者中止任务。
第三层:撤销已经发生的外部副作用
这一层最难。
如果 Agent 已经:
发送了邮件
创建了日历事件
提交了订单
修改了数据库
调用了支付接口
用户现在按停止,现实不会自动倒退。
所以必须记住:
Cancel 不等于 Rollback。
停止模型,只是让它不要继续做下一步。
已经发生的事情,必须用另外的机制处理。
在分布式系统中,这类问题通常通过补偿事务解决。
例如一个流程包含:
创建订单
↓
锁定库存
↓
发起付款
如果付款失败,无法使用一个简单数据库事务把所有外部服务恢复到原始状态。
系统只能执行反向补偿:
付款失败
↓
释放库存
↓
取消订单
Agent 工具同样应该考虑补偿能力。
例如:
create_calendar_event
可以通过:
delete_calendar_event
进行补偿。
而:
send_email
通常无法真正撤回。
最多只能再发送一封更正邮件。
这说明不同工具的可逆性完全不同。
除此之外,还要解决一个极其危险的问题:重复执行。
假设 Agent 调用付款接口。
付款已经成功,但网络响应丢失了。
Agent看到超时,以为失败,于是自动重试。
结果可能变成重复付款。
所以带副作用的工具必须使用幂等键。
例如:
task_id = task_123
step_id = payment_04
idempotency_key = task_123_payment_04
即使同一个请求因为网络问题被发送两次,外部系统也只执行一次。
因此,一个真正可靠的 Agent 任务系统至少应该记录:
任务 ID
步骤 ID
当前状态
幂等键
执行结果
重试次数
补偿动作
到了这里,Agent 已经不再只是“LLM 调用工具”。
它开始变成一个工作流引擎。
六、支持多个模型,远不只是换一个 base_url
Hermes 能够接入不同模型服务。
表面看起来像:
model = "xxx"
base_url = "xxx"
api_key = "xxx"
但真正的 Provider 适配远没有这么简单。
不同模型服务可能使用不同的:
消息格式
Tool Call 格式
Reasoning 字段
Streaming 事件
缓存控制方式
角色顺序要求
Token 统计逻辑
异常类型
如果 Agent Loop 直接理解每一家 Provider 的格式,核心代码很快就会充满判断:
if provider == "openai":
...
elif provider == "anthropic":
...
elif provider == "other":
...
随着 Provider 增加,整个 Runtime 会越来越难维护。
更好的方式是引入统一的内部消息表示。
例如 Agent 内部始终使用:
{
"role": "assistant",
"content": "...",
"tool_calls": []
}
然后由 Provider Adapter 转换成目标服务需要的格式。
这个设计很像编译器。
编译器不会让每一种高级语言直接适配每一种 CPU。
而是:
C / Rust / 其他语言
↓
中间表示 IR
↓
x86 / ARM / 其他架构
Agent Runtime 同样可以:
统一消息 IR
↓
Provider Adapter
↓
OpenAI / Anthropic / 其他模型
核心控制循环只理解稳定的内部协议。
变化被封装在系统边界。
这比“支持多个模型”更有价值。
它体现的是一个经典的软件工程原则:
把变化隔离在适配层,让核心逻辑依赖稳定抽象。
七、为什么 Tool Call 的消息顺序不能随便改
很多人把 Agent 的历史消息看成普通聊天记录。
实际上,一旦引入 Tool Call,消息历史已经变成一份执行事件日志。
标准过程类似:
User:提出任务
Assistant:请求调用工具 A
Tool:返回工具 A 的执行结果
Assistant:根据结果继续处理
工具请求和工具结果通常通过 tool_call_id 建立对应关系。
如果只保留 Tool Result,却删除前面的 Tool Call,系统就不知道这个结果对应哪一次请求。
如果一个 Assistant 发起了两个工具调用:
call_1
call_2
后面必须能够明确返回:
result_for_call_1
result_for_call_2
这和函数调用很像:
result = function(arguments)
不能只有一个 result,却不知道它来自哪个 function、用了什么 arguments。
因此,Agent 的消息存储不能只保存最终文本。
还要保存:
工具调用参数
工具调用 ID
工具执行结果
Reasoning 信息
错误状态
模型使用量
它越来越像 Event Sourcing。
系统通过一连串事件,还原任务发生过什么。
这也是为什么随意删除某段 Tool Call,可能导致整个会话无法正确回放。
八、不是所有 Tool 都属于同一个层级
Hermes 中有些能力表面看也是工具,例如:
memory
todo
session_search
delegate_task
但它们和搜索网页、查询数据库、生成 PPT 并不相同。
前者操作的是 Agent 自己。
后者操作的是外部世界。
可以借用 Kubernetes 的概念理解。
Control Plane
负责系统自身的控制:
保存记忆
管理任务
恢复会话
调度子 Agent
压缩上下文
Data Plane
负责真正的业务执行:
查询订单
读取面料资料
生成图片
调用 Canva
发送邮件
为什么要分开?
因为这两类工具的风险、权限和一致性完全不同。
memory.write 失败,影响的是 Agent 未来会不会记住某件事。
payment.execute 失败,影响的是真实资金。
delegate_task 创建了一个子任务,属于 Runtime 调度。
delete_database_record 修改了业务数据,属于外部副作用。
如果所有东西都被当成普通 Tool,系统很难针对不同类型设置:
权限
审批
日志
重试
回滚
生命周期
一个成熟的 Agent Runtime,需要明确区分:
哪些调用是在控制 Agent,哪些调用是在改变现实。
九、Hermes 的“自我改进”,其实不是模型学会了新知识
Hermes 的一个重要能力是把成功经验沉淀成 Skill。
很多介绍会把它称作:
Agent 可以自我学习、自我进化。
这句话容易产生误解。
因为大多数情况下,模型参数并没有发生变化。
真正发生的是:
系统原来没有这个 Skill
↓
Agent 执行任务
↓
经历失败和修正
↓
把有效流程写入 SKILL.md
↓
未来任务加载这个 Skill
↓
模型输入发生变化
↓
未来行为发生变化
所以更准确的说法是:
系统通过修改外部策略资产,改变未来模型的条件输入和行为分布。
这是一种外部学习,而不是参数学习。
它的优点很明显:
成本低
可以查看
可以修改
可以做版本 Diff
可以人工审批
可以回滚
但它也带来了一个更危险的问题:错误经验固化。
假设某次接口异常,Agent 连续重试五次以后成功了。
它可能把经验总结为:
遇到接口错误时,应连续重试五次。
但真实原因可能只是网络短暂抖动。
这条错误 Skill 一旦进入长期策略库,未来每一次异常都可能触发无意义重试。
因此,一个真正可靠的 Skill,不应该只是一段“经验笔记”。
它应该接近一个可以治理的业务程序:
# Fabric Promotion PPT
## 适用条件
客户需要面料推广方案时使用。
## 输入要求
- 客户品牌
- 目标品类
- 面料候选清单
- 检测报告
## 执行步骤
1. 判断客户定位与价格带
2. 筛选 3—5 款候选面料
3. 从检测报告提取可验证卖点
4. 生成 Proof Card
5. 调用 PPT 工具生成初稿
6. 检查技术表述与证据来源
## 质量标准
- 所有性能数字必须有来源
- 无法验证的数据标记为 Information Not Available
- 不得把实验室指标直接等同于成衣效果
## 失败处理
- 缺少检测报告时停止生成技术结论
- PPT 工具失败时保留结构化文案
这时,Skill 已经不只是提示词。
它包含:
触发条件
输入契约
执行流程
质量门槛
异常处理
禁止事项
它正在接近一段可执行的业务逻辑。
十、没有治理的“自我改进”,可能只是自我污染
随着 Agent 不断创建 Skill,策略库会逐渐出现:
重复 Skill
冲突 Skill
过期 Skill
从未使用的 Skill
粒度过细的 Skill
错误 Skill
例如:
canva-ppt
canva-report
management-ppt
business-presentation
ppt-with-canva
它们可能描述的是同一件事,只是名称不同。
Skill 越多,模型选择正确 Skill 的难度反而越高。
所以 Hermes 中 Curator 这类机制非常重要。
它的本质不是简单整理文件,而是在做 Agent 知识库的垃圾回收和生命周期管理。
成熟的 Skill 系统至少要区分:
active
stale
archived
deprecated
并记录:
谁创建的
基于哪一次任务创建
最后一次使用时间
被使用多少次
成功率如何
适用于哪个业务范围
当前版本是什么
更严格的企业流程应该是:
执行轨迹
↓
提取候选经验
↓
测试验证
↓
人工审批
↓
版本化发布
↓
监控效果
↓
归档或回滚
没有测试、审批和版本控制的自我改进,严格来说不能叫改进。
更准确地说,那只是:
Agent 拥有了修改自己未来行为的能力。
至于改得更好还是更坏,并没有保证。
十一、Hermes 做到了哪里,又没有做到哪里
理解一个开源项目,不能只会赞美。
还要能判断它的设计边界。
Hermes 的架构非常适合:
个人 Agent
研发实验
单机或有限规模部署
中等并发
文件化 Memory 和 Skill
长时间运行的本地 Agent
它使用的很多设计都有明确优势:
SQLite 部署简单
本地文件容易查看
线程池容易实现
中央 Agent 编排容易调试
Sandbox 可以快速隔离执行
但当系统进入多租户、大规模企业平台后,会出现新的问题:
多个节点如何争抢同一个任务
执行节点宕机后任务由谁接管
如何判断任务仍然存活
如何实现租户隔离
如何管理跨节点锁
如何记录完整审计日志
如何统一管理 Skill 版本
如何在不同服务器共享 Artifact
这时,仅靠单机 Runtime 已经不够。
企业级架构可能需要进一步拆成:
Agent API
任务编排服务
模型网关
Tool Runtime
Sandbox Service
Memory Service
Skill Registry
RAG Service
Artifact Storage
Audit Service
还需要引入:
消息队列
任务租约
心跳机制
分布式锁
RBAC
统一审计
版本治理
失败恢复
所以我对 Hermes 的判断是:
它是一套非常值得研究的、单机优先的有状态 Agent Runtime,但不是企业级分布式 Agent 平台的最终形态。
它真正有价值的地方,不是可以直接照搬,而是告诉我们:
长期 Agent 系统的问题应该怎样分层,哪些状态应该分开,哪些能力必须进入 Runtime,而不能继续依赖 Prompt。
十二、拆完 Hermes,我真正学到的五件事
1. Agent 的核心不是模型,而是状态
模型可以从一家 Provider 切换到另一家。
但任务执行到哪里、哪些动作已经完成、哪些结果已经持久化,这些状态一旦混乱,再强的模型也无法补救。
2. 上下文不是数据库
Prompt 只是模型当前看到的工作区。
关键业务状态必须进入结构化系统,不能依赖模型“记得”。
3. Tool Call 不是普通函数调用
只要工具能够改变现实,就必须考虑:
并发
幂等
事务
补偿
审批
权限
审计
4. 停止任务不等于撤销现实
模型生成可以停止。
已经发出的邮件、提交的订单和产生的付款不会自动消失。
5. 自我改进必须可验证、可审批、可回滚
否则 Agent 只是在更高效地积累错误。
结语:真正的 Agent,是一个非确定性大脑外面的确定性系统
大模型有一个非常特殊的特点:
它很聪明,但它不是确定性的。
同一个问题,两次回答可能不同。
它会遗漏信息,会误判工具,会生成不完整计划,也可能因为上下文变化而改变决策。
但真实业务系统不能完全依靠这种不确定性运行。
订单不能“可能创建成功”。
付款不能“模型觉得大概完成了”。
审批不能“上下文摘要里好像通过了”。
所以 Agent 工程真正要做的,是在一个非确定性大脑外面,建立一套尽可能确定的系统:
用状态机约束任务
用数据库保存事实
用 Tool Schema 约束动作
用权限限制能力
用 Sandbox 隔离风险
用幂等保证重试安全
用补偿处理失败
用审计记录全过程
用 Skill 沉淀可复用经验
这也是我拆开 Hermes 之后,得到的最大启发。
Hermes 最值得学习的,不是它接入了多少工具,也不是它支持多少模型。
而是它试图回答一个更本质的问题:
如何让一个无状态、非确定、上下文有限的大模型,长期执行有状态、有副作用、需要恢复和治理的真实任务?
当你开始认真回答这个问题时,你做的就不再是一个聊天机器人。
你正在构建一个真正的 Agent Runtime。
更多推荐


所有评论(0)