做Agent最怕的是什么?是用户问一句,它回一句漂亮话,然后什么实事都没干。我这两年在内部项目里反复打磨一个代号叫Hermes Agent的智能体框架,折腾得最多的就是四件事:会话、工具、MCP、记忆。这四个词拆开看都不难理解,但真正把它们揉进一个系统里,让它们互相配合而不是互相打架,坑远比想象的多。
这篇文章不打算讲某个大厂的完整方案,就讲讲我在设计 Hermes Agent 时对这四层能力的拆解、取舍和实测经验。希望能给正在搭 Agent 或者准备重构 Agent 架构的朋友一点参考,少走几步弯路。
1. Agent能力分层:为什么跳不过会话、工具、MCP、记忆这四块基石
1.1 从一次真实调用链路看四层能力的协作
先看一个我经常用来跟团队举例子的场景。用户对 Agent 说:“帮我整理上个月的销售报表,把结论摘要同步给团队成员,顺便记住我以后周报都要这个格式。”
这句话看起来简单,但内部至少发生四层联动:
- 会话层要理解这是多轮对话中的新请求,还要识别出里面其实藏着三个子意图:查数据、生成摘要、发送通知。
- 工具层要决定调用哪些函数:先调销售数据查询,再调文本摘要生成,最后调消息发送。而且这三步有先后依赖,不能乱序执行。
- MCP层要保证 Agent 能发现这些工具、按标准格式把工具描述送给模型、再把模型生成的调用参数安全地传递到真实服务上。
- 记忆层要判断“我以后周报都要这个格式”是一条值得长期保存的偏好,把它写进用户档案,下次周报任务自动生效。
如果四层各自为政,这个链路会裂得很厉害。最典型的问题就是:会话层把上下文丢给工具层,工具层不知道这段记忆里“用户已经指定过格式”,于是又生成一份完全不符合偏好的日报。所以我在设计 Hermes Agent 时,第一原则就是把这四个模块当作一个整体来设计接口,而不是四个独立服务。
1.2 设计原则:能力解耦与依赖关系
四层的定位我这么看:
- 会话层是骨架。它负责状态流转、上下文保存、用户意图的连续性。
- 工具层是执行器。它把模型的想法变成真实动作。
- MCP层是连接器。它让工具接入从“每家一套私有协议”变成“一套标准协议”。MCP 是模型上下文协议的英文缩写,它的目标很朴素:让 Agent 和外部工具之间,用统一的格式完成能力发现、调用和结果回传。
- 记忆层是档案室。它让 Agent 不是每次都从零开始,而是带着用户历史的“惯性”去完成任务。
依赖关系上,我尽量保持单向依赖:会话层可以调用工具层和记忆层,工具层依赖 MCP 层去执行外部服务,记忆层对会话层提供上下文增强,但不会反向去指挥会话层的状态机。这样每层都能独立测试,出问题时定位也快。
2. 会话层:上下文管理是Agent体验的第一道坎
2.1 多轮会话的状态机设计
很多初学者做 Agent 时,把会话理解成“一个消息列表,不停地往后面追加”。这在简单问答里没问题,但一旦涉及工具调用,就会出乱子。
比如用户问“现在几点了”,Agent 调用时间服务,拿到结果后回复用户。这个过程里会话其实经历了四个状态:等待用户输入、模型推理中、等待工具返回、生成最终回复。如果不把这些状态显式建模,就会出现一个问题:工具返回结果之后,模型不知道它是应该直接回答,还是应该再追问用户。
我在 Hermes Agent 里用一个简单的状态机来描述会话生命周期:
@dataclass class SessionState: session_id: str phase: str # waiting_input / reasoning / waiting_tool / awaiting_confirmation pending_tool_calls: list context_snapshot: str关键的一点是:工具调用不是聊天内容,它是会话中间的暂停点。有一次线上事故,就是因为某个 Agent 在等待工具返回时,用户又发来一条新消息,系统直接把两条消息拼在一起送进模型,导致模型把工具结果和用户新消息混在一起生成了荒谬回复。后来我加了一个规则:只要会话处于waiting_tool状态,新到的用户消息先进入排队区,等当前工具调用闭环后再决定如何处理。
2.2 上下文窗口的精打细算
上下文窗口是硬约束。我见过不少项目把整个对话历史、全部工具描述、一大段记忆一股脑塞给模型,结果窗口爆掉不说,模型反而因为信息过载,开始胡言乱语。
我一般把上下文预算分成五块:
| 分区 | 内容 | 经验占比 |
|---|---|---|
| 系统提示 | Agent 角色、行为规则 | 5% - 10% |
| 工具描述 | 当前会话可调用的工具清单 | 15% - 25% |
| 记忆注入 | 用户偏好、相关事实、历史摘要 | 10% - 20% |
| 近期对话 | 最近几轮的原始消息 | 35% - 50% |
| 中间推理 | 模型输出框架占位 | 5% - 15% |
这里有三个实际经验:
第一,工具描述不能每轮都全量塞。我会在会话开始时加载工具清单,但如果后续轮次没有新增工具,就可以用一轮“工具已加载”的短标记替代,省下来的 token 留给对话历史。
第二,早期对话要滚动摘要。滚动摘要不是说简单地把旧消息截断,而是让模型定期把前几轮对话压缩成一段结构化摘要,保留人物、任务、关键偏好。例如摘要里应该写“用户提到预算上限是五千”,而不是“用户说自己最近有个项目”。
第三,记忆注入要有优先级。不是所有记忆都值得每次进上下文。Hermes Agent 的做法是:从记忆层检索出与当前会话最相关的 3 到 5 条事实,用一个专门的记忆区段注入。
2.3 流式输出与会话恢复的实战细节
流式输出是交互体验的重要缓解器,但做了流式之后,工具调用阶段有个容易忽略的细节:工具调用本身要不要流式展示给用户?
我的建议是:给用户看到中间状态,而不是只看到最终结果。比如 Agent 在调销售数据服务时,可以在界面上展示“正在查询销售数据”,等数据返回后再继续生成最终答案。这个设计对长任务尤其重要,否则用户看着光标转圈五秒钟,早就焦虑地刷新页面了。
会话恢复方面,我踩过一个蛮痛的坑。某次服务重启后,一半用户的会话加载出来,Agent 说的上句话和上下文完全对不上。原因是快照只存了消息列表,没存状态机的phase和pending_tool_calls。
现在 Hermes Agent 的做法是:每完成一个“回合”(从接收用户输入到形成最终回复)就存一次会话快照,快照里包含消息列表、当前状态、还有未完成的工具调用清单。这样即使服务重启,也能从最近的快照恢复,而不是从一条孤零零的旧消息开始。
3. 工具层:让模型从"会说话"变成"会办事"
3.1 函数声明的边界
工具层最核心的判断点,是函数声明写得够不够清晰。模型不是程序员,它不会看你的函数源码,它只能靠函数名、描述和参数结构来猜测怎么调用。你声明写得好,调用准确率就高;声明写得含糊,后面就是无穷无尽的参数幻觉。
我总结了一份实际有效的函数声明七要素:
| 要素 | 说明 | 容易犯的错 |
|---|---|---|
| 函数名 | 清晰的动作 + 对象 | 起得过于模糊,如process_data |
| 摘要 | 一句话说明函数行为 | 写成命令式而非行为描述 |
| 参数名 | 跟业务概念对齐 | 用a、b这类无意义名称 |
| 参数类型 | 基础类型 + 结构类型 | 该用 object 时用了 string |
| 必填标记 | 哪些参数必须给 | 漏标必填导致运行时缺参 |
| 枚举值 | 只允许哪些取值 | 枚举描述被写进正文而非枚举字段 |
| 示例 | 给一个可参考的参数 JSON | 完全没有示例 |
我放一个典型的声明片段,方便你直观感受“写得好”是什么样:
{ "name": "send_team_notification", "description": "向指定团队的成员发送一条通知消息,用于任务同步、进度提醒或结论分享。当用户需要将信息告知团队其他成员时使用。", "parameters": { "type": "object", "properties": { "team_id": { "type": "string", "description": "目标团队的唯一标识,如 'team_sales'" }, "message": { "type": "string", "description": "要发送的通知正文。建议用清晰简洁的语言表达核心信息。" }, "priority": { "type": "string", "enum": ["normal", "high"], "default": "normal" } }, "required": ["team_id", "message"] } }这里的核心经验是:描述里要写“什么时候用”,而不只是“这个函数能做什么”。模型做工具选择的依据,本质上就是函数描述和用户意图的语义匹配。你写上“当用户需要将信息告知团队其他成员时使用”,比单纯写“发送通知”命中率高一截。
3.2 工具路由与并行调用的调度策略
模型在一次回复里可能同时请求调用多个工具。比如“查一下天气,再帮我订个提醒”,模型可能会并行请求两个函数调用。这时候调度层就需要判断:这两个调用是相互独立的,还是存在依赖关系。
独立调用可以并行发出,节省总耗时;有依赖的调用必须按序执行。Hermes Agent 里我用一个简单的依赖图来管理:
- 先扫描所有工具调用的输入参数,看某个调用的输出是否是另一个调用的输入。
- 如果没有依赖,全部并行。
- 如果有依赖,拓扑排序,等前置调用完成后再执行后续调用。
一个常见的反例是:Agent 先调“创建任务”,又调“给任务分配负责人”,后者依赖前者的任务 ID。如果并行执行,“分配负责人”这个调用就会拿到一个空引用。我在调度器里会缓存前一轮调用的结构化返回值,用来填充后续调用的依赖参数。
还有一个经验跟超时有关:并行调用虽然快,但单次调用超时会导致整个回合卡住。我给每个工具调用设置了独立的超时时间,默认 15 秒,超时后立即返回一个“工具调用超时”的错误给模型,让模型决定是重试还是换方案,而不是整个会话挂起。
3.3 工具失败后的自愈:重试、降级与错误反馈回路
工具一定会失败,而且失败方式五花八门:服务超时、参数校验不过、下游系统返回 500、权限过期。工具层的设计目标不是“让工具不失败”,而是“失败之后 Agent 还能体面地继续”。
我的做法是把错误变成一个模型可读的反馈:
- 错误文本要结构化,包含错误码、可读信息、可能原因。
- 不要把完整堆栈塞给模型,模型能处理的是业务语义,不是调用栈。
- 给模型提供“可执行的下一步建议”,比如“这个接口限流了,可以等待 10 秒后重试”。
举一个实际例子:某个 Agent 在调用报表服务时,数据库连接池满了,返回超时。如果直接给模型抛一段堆栈,模型会胡编一个理由。而 Hermes Agent 的做法是返回:
tool_result_error: DB_POOL_EXHAUSTED message: 数据库连接池已满,无法执行查询 suggestion: 您可以稍后重试,或者缩小查询时间范围后再试模型读到这样的反馈后,通常会生成一句“系统暂时繁忙,我再试一次”,然后发起重试,或者主动询问用户是否要缩小时间范围。这套机制把“工具失败”变成了“ Agent 的对话素材”,体验上比直接报错好很多。
4. MCP层:接入协议化工具生态的姿势
4.1 MCP解决了什么
在 MCP 出现之前,每接一个外部工具就要写一套胶水代码:A 服务用 REST,B 服务用 WebSocket,C 服务自己定义了一套推送协议。于是工具层的代码越来越臃肿,每加一个工具,改动面就涉及接入、鉴权、数据格式转换三块逻辑。
MCP 的意义在于把“工具接入”从私有实现变成了标准协议。它的核心能力可以概括成三点:
- 能力发现:客户端可以获取服务端支持哪些工具、每个工具有什么参数。
- 统一调用:所有工具的调用和结果返回,都走同一套消息结构。
- 资源与上下文扩展:除了工具,还可以暴露可读取的资源片段、提示模板等。
我倾向于把 MCP 类比成 USB 接口——它第一次让“插上就能用”成为可能。只要你实现了 MCP 服务端,任何支持 MCP 的客户端都能发现并调用你的工具,不需要为每家客户端单独写适配。
4.2 连接生命周期
MCP 连接不是一次 HTTP 请求就结束的简单过程。客户端连接一个 MCP 服务端后,要经历这么几个阶段:
- 初始化握手:双方交换协议版本和实现信息。
- 能力协商:确认服务端支持哪些能力,比如是否支持工具调用、是否支持日志、是否支持采样。
- 工具列表拉取:客户端拿到服务端提供的工具清单。
- 消息收发:进入正常的工具调用和结果返回阶段。
- 连接维护与断开:处理心跳、重连、优雅退出。
传输方式上,最常见的两种是标准输入输出(stdio)和 HTTP SSE。我给 Hermes Agent 的选型经验是:本地进程工具用 stdio,简单直接;跨网络服务用 HTTP,方便部署在独立服务上。
这里有个大家容易忽略的点:MCP 的数据交换格式是 JSON-RPC 风格的请求响应结构,工具调用结果里要能区分“成功返回值”和“错误信息”。我见过一些服务端把所有结果都塞在返回值里,连 HTTP 状态都不区分,结果客户端很难做异常分流。
4.3 工具发现的动态更新与权限边界
MCP 的一个优势是工具列表可以动态变化。服务端运行时可以新增或移除工具,客户端可以通过通知机制感知变化。但在实际系统里,这个“动态”很容易变成事故源头。
我踩过的一个坑是:某个 MCP 服务在会话中途新增了一个工具,Agent 立刻就在下一轮调用了它,结果因为新工具还没有完成权限配置,直接拒绝了请求。后来我把工具的“可用状态”和“权限状态”分开管理:
- 可用状态由 MCP 服务端声明,用于表示协议上是否可被调用。
- 权限状态由 Agent 侧的授权中心判定,用于决定当前会话是否有权调用某个工具。
换句话说:MCP 告诉 Agent “有这个工具”,不代表 Agent 就可以调用它,还要过一遍权限检查。我在授权中心维护了一张“工具到角色的映射表”,只有映射通过,工具才会被真正列入模型的可用清单。这样既享受了动态发现的好处,又规避了未授权调用的风险。
权限边界还有一个细节:工具调用的输入参数也要做白名单校验。比如“发送邮件”工具的收件人字段,如果模型受提示词注入影响,试图把邮件发给某外部地址,系统应该在执行前拦截。
5. 记忆层:从"忘记上下文"到"真正了解用户"
5.1 短期记忆与会话窗口的关系
很多人把“记忆”等同于上下文窗口,其实这是两个层次。
会话窗口是短期记忆的容器,它保存的是当前会话里最近几轮的原始消息和中间结果。但窗口有大小上限,而且里面的信息是混杂的——既有用户真正表达过的事实,也有模型的推理过程、临时的噪音信息。Hermes Agent 的做法是:会话进行中,短期记忆就是窗口里的内容;会话结束时,我会做一次“记忆提炼”,从窗口里把值得长期保存的关键信息抽取出来,写入长期记忆层。
这么说吧,短期记忆是“这次聊天的便签”,长期记忆是“这个人真正值得记住的事实”。
5.2 长期记忆的分层存取
长期记忆如果只用一个向量库,效果会很一般。我的经验是分成三类存储:
| 记忆类型 | 示例 | 推荐存储 |
|---|---|---|
| 用户画像 | 偏好、身份、角色 | 结构化 KV 或数据库表 |
| 事实清单 | 正在跟进的项目的关键事实、截止日期、预算 | 结构化记录 + 时间戳 |
| 交互历史 | 过去任务的摘要、用户反馈记录 | 向量库,用于语义检索 |
只靠向量检索的一个问题是:向量相似度高的结果不一定是对的。比如用户上周三说“这周先不发周报”,今天问“我的周报习惯是什么”,模型如果从向量库里检索到“不发周报”这条记录,就会给出互相矛盾的回答。
我的对策是给每条记忆打上时间戳和置信度,并在检索结果里对时效敏感的字段做额外过滤。凡是写“暂时”“本周”“这次”这类时间限定词的记忆,默认标记为短时效,检索加权降低。
5.3 记忆的写入时机与隐私边界
记忆什么时候写,比怎么存更值得花时间想。我总结的经验是:不要在对话中途频繁写入长期记忆,只在回合边界做“候选记忆评估”。
具体流程是:
- 每个回合结束后,把当前窗口里的关键信息抽取成若干条候选记忆。
- 逐条判断:
- 是否是用户明确表达的偏好或事实?
- 是否与已有记忆重复?如果重复,新旧信息哪个更可信?
- 是否是临时性信息?如果是,写入短期记忆区。
- 通过判断的候选记忆,才写入长期记忆库。
这个机制避免了“用户随口说一句,Agent 就记一辈子”的尴尬。有一次用户在半开玩笑地说“我才不喜欢喝咖啡”,如果系统不加判断地写入画像,后面所有推荐都会被咖啡带偏。
隐私边界方面,我会在 Agent 设置里提供一个“记忆管理”入口,用户能查看 Agent 记住了什么、删除某条记忆或清空全部记忆。这个设计在真实使用里重要程度比我预想的高——用户一旦发现自己控制不了记忆内容,很快就会失去信任。
6. 四层协同实测:我在落地Hermes Agent时踩过的坑
6.1 记忆污染如何影响工具调用准确性
记忆层对工具层的影响,比很多人想象的大。
有一次线上反馈说 Agent 在给用户订机票时,一直把目的地填成上海,明明用户这次说的是北京。排查后发现:长期记忆里存储了一条“用户常住上海”的画像,模型在填写目的地参数时,优先参考了这条记忆,与用户当前明确表达冲突了。
这个问题让我反思了一个原则:用户当前会话的显式指令,永远优先于历史记忆。现在 Hermes Agent 会先把当前轮次里的明确指令做一次“指令锚定”,提取出不可被记忆覆盖的字段,再调用工具。这条经验后来成了我的默认规则:记忆是建议,不是指令。
6.2 会话快照在MCP断线恢复中的作用
还有一次比较典型的故障:MCP 连接因为网络抖动断开了,某个工具调用正在进行中。由于会话快照没有完整保存调用状态,恢复后工具其实执行成功了,但 Agent 不知道结果,于是又重新调用了一次,导致发了两封一样的通知邮件。
这个事故让我做了两个改动:
- 工具调用增加幂等键:每次调用带上一个唯一操作 ID,服务端记录已经执行过的 ID,重复请求直接返回第一次的结果。
- 会话快照里保存“进行中的工具调用 ID 列表”,MCP 重连后先查询这些调用的完成状态,再决定是补取结果还是重新执行。
经过这个改造,MCP 断连不再是可怕的故障,而变成一个可以被 Agent 自动修复的普通异常。
6.3 调优清单与性能基线
最后分享一份我实测后整理出的调优清单,也许能帮你省下几轮无方向的调参时间。
| 关注项 | 基线建议 | 说明 |
|---|---|---|
| 首 token 延迟 | 1 秒内 | 超出时优先检查工具描述是否过长、记忆注入是否过重 |
| 工具调用准确率 | 90% 以上 | 低于此值时检查函数声明的描述质量 |
| 记忆写入量 | 每个任务不超过 5 条 | 太多会稀释长期记忆的针对性 |
| MCP 连接重连时间 | 2 秒内完成 | 超出时检查服务端的 ping/pong 配置 |
| 上下文窗口利用率 | 80% 以下 | 接近上限时优先做历史消息摘要 |
性能基线不是死的,不同的业务场景差别很大。我建议你先跑一周真实流量,记录每个指标的分布,再根据 p95 去调优,而不是追求“所有指标都好看”。
我对这四层能力的最终理解是:会话给了 Agent 灵活应变的样子,工具让它能把想法变成行动,MCP 让它能轻松接入真实世界已有的服务,记忆则让它从一个“每次都重新认识的陌生人”,慢慢变成一个“越来越懂你的助手”。四者合起来,才是我心里 Agent 真正意义上的“能力成型”。希望这篇分享能让你少踩几个我踩过的坑。