ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

智能体核心能力拆解:会话、工具、MCP与记忆的编排实践

智能体核心能力拆解:会话、工具、MCP与记忆的编排实践

1. 四个能力块如何组成一个智能体

我在接触 Hermes Agent 这类 Agent 框架时,第一反应是它跟普通聊天机器人到底差在哪里。普通聊天机器人只有“会话语义”,最多再挂一个知识库,而 Agent 真正值钱的地方是它能把会话、工具、MCP、记忆这四样东西串在一起。会话负责理解人和表达意图,工具负责真正动手做事,MCP 负责把外部系统变成统一的接线口,记忆负责把一次性的交流沉淀成可复用的经验。

这四个能力块孤立看都不难,难的是组合。会话模块如果做得太薄,工具调用的上下文就会乱,经常出现模型在前一轮已经知道库存不足,下一轮又用旧数据回答;工具模块如果太散,每个外部接口都要手写调用函数,维护成本会指数上升;记忆如果不分层,要么全塞进上下文把 token 耗尽,要么本来该记住的用户偏好一句话都想不起来。所以我更愿意把 Hermes Agent 看作一套调度框架,而不是单纯的模型封装。

1.1 会话是总调度台

在 Hermes Agent 里,会话层承担的是“总调度台”的角色。模型本身是无状态的,每次推理只看到你给它的 prompt,如果没有会话层处理,它根本不知道自己刚才说过什么。会话层要维护多轮消息列表、保存用户身份、记录当前场景状态,还要在每次推理前决定哪些历史内容值得放进上下文。

我在实际项目里经常把会话层比作前台的接待员。用户进来说“帮我查上次说的那批货”,接待员得知道“上次”指的是哪一次、查的是哪批货、用户是什么权限,然后才能去调后续服务。如果这个接待员把所有内容都记在便签纸上,一百轮对话以后便签纸就糊成一片了,所以会话层一定要有清理和摘录的机制。

1.2 工具让模型有手

工具调用或者说 function calling,是 Agent 从“能说”变成“能做事”的关键。Hermes Agent 会把一组工具以结构化的方式暴露给模型,模型在生成回复时,如果判断到当前任务需要查询数据、写文件、发请求,就会生成一个“调用某某工具、参数是什么”的信号,而不是直接输出一段猜测。

没有工具的时候,模型回答“当前库存是多少”只能靠猜或者说“我不确定”;有工具之后,它可以真的去查询数据库,把准确数字拿回来再做回答。工具模块看起来只是对外部函数的包装,实际上它决定了 Agent 能在多大程度上接管真实业务。

1.3 MCP 是把外部能力变成统一协议

MCP 全称是 Model Context Protocol,它最吸引我的一点是解决了工具接入的“重复造轮子”问题。早期我为某个内部系统接工具,每接一个数据库就要写一个查询函数,每接一个文件服务就要写一套读写封装,而且每个函数的返回格式都不一样,模型收到的信息五花八门,很难做二次判断。

MCP 相当于把外部服务统一成一套协议:服务端提供工具、资源、提示词模板,Agent 通过标准接口去发现和调用。在 Hermes Agent 里接 MCP server,就像给电脑插一个标准 USB 设备,而不是每个外设都自己焊线。具体配置我后面会讲,但先要理解它的定位:它不是工具调用的替代方案,而是工具调用的标准化底座。

1.4 记忆让交流变成有沉淀的

会话是短时记录,用完即走;记忆是长时沉淀,需要跨会话重用。Hermes Agent 的会话模块如果只负责多轮上下文,它并不能帮助用户建立“这个助手越来越懂我”的体验。只有把用户偏好、历史结论、业务约束存进记忆库,下一次对话才能据此调整回答。

我在某个内部项目里遇到过很典型的问题:用户每次进来都要重复自己的筛选偏好,比如“只看华东仓现货”“批量要200件起”,因为会话窗口早就刷新了。后来做了记忆层,把这些偏好抽取出来存好,开新对话时自动注入,体验立刻不一样。记忆不是简单地存聊天记录,而是要抽取关键事实、做向量化、做优先级管理,这块我会在第五章展开。

2. 会话模块的落地要点

很多人以为会话模块就是把用户消息和助手回复追加到一个数组里,然后整个丢给模型。在小型 Demo 里这样做没问题,但一旦进入真实业务,你会很快撞上上下文窗口不足、会话状态错乱、多用户数据串味三个问题。下面这几点是我在 Hermes Agent 里落地会话模块时觉得最值得关注的部分。

2.1 上下文窗口与滑窗压缩

模型上下文窗口是有上限的,哪怕支持几十万 token,也不能真的把所有历史消息都堆进去,因为越靠后的信息被注意力覆盖得越弱,而且成本会明显上升。我在项目里常用的策略是滑动窗口加摘要压缩。滑动窗口的意思是保留最近 N 轮完整消息,比如最近 10 轮用户和助手的对话;超过窗口的部分,我让会话层定期生成一段摘要,存成一条“记忆摘要消息”放在消息列表开头。

这样模型看到的是“摘要 + 最近10轮完整内容”。摘要里可以包含用户的主要目标、已经确认的事实、尚未完成的事项。比如用户半小时前问过某个商品的库存,之后又聊了别的,窗口滚动后那条细节可能不在完整消息里,但摘要会写上“用户曾查询商品 SKU 为 xxx 的库存”,后续提问依然可以衔接。

这里的参数选择我一般看总 token 量来定。如果每轮对话平均 800 token,窗口保留 12 轮,摘要假设 600 token,那模型每次约消耗 10200 token 的对话部分。再留出用户当前问题、系统提示和工具返回的余量,整体就能控制在模型接受范围内。实际使用中要留 20% 左右富余量,否则一次性塞入超大工具返回值时很容易爆上下文。

提示:我习惯在会话层记录每个 session 的 token 估算值,比如通过字符数或模型 tokenizer 工具粗略统计,而不是等请求发出去才发现超限。每次追加消息时顺手累计,超阈值就自动触发摘要滚动。

2.2 会话状态的持久化与隔离

会话状态不只是消息列表,还包括当前处于什么流程阶段、用户填了哪些必填参数、上一次工具调用是否成功。比如用户正在走一个“提交请假申请”流程,中间问了一句别的,模型应该能回到原流程继续。Hermes Agent 里我会用一个 JSON 对象保存会话状态,每次推理之后更新一次,持久化到内存数据库或本地文件。

多用户隔离方面,最基础的方案是用 session_id 作键。每一条消息都打上 session_id,会话层按 id 取对应上下文。但要注意 session_id 不能只用用户的 ID,因为同一个用户可能开多个会话。比如用户想同时比较两条商品的购买流程,如果只有一个会话,状态就会互相覆盖。我用的是 user_id + 会话序号组成的复合键,确保一个对话流独立一个上下文。

状态持久化还有一个容易被忽视的点:会话续期。如果用户隔了三天再回来,完全清空历史会让体验断档。我会设置“热会话”和“冷会话”的概念:最近 30 分钟活跃的会话热保留,超过一天的会话归档,但归档时留下结构化摘要和关键偏好,下次开启新会话时先加载用户级长期记忆,而不是把旧消息原样搬回来。

2.3 流式输出如何影响页面交互

会话模块除了把消息发给模型,还要处理输出的流式返回。终端用户对 Token 流没有概念,但他们都讨厌长时间转圈等待。如果模型完整生成几百字再一次性返回,用户可能等 10 秒才看到第一个字,体感非常差。Hermes Agent 里我接入 SSE 或 WebSocket 通道,把生成结果分块推给前端。

流式输出还牵扯到工具调用的展示。当模型决定调用工具时,我会在流里发一个特殊事件给前端,让界面先展示“正在查询库存…”这类提示,而不是让用户干等。工具执行完成后,再把结果作为一条 assistant 消息续进对话流。这个设计虽然只是交互细节,但在真实使用中非常影响用户对“智能感”的感知。

3. 工具调用:把模型输出变成真实动作

工具调用的基本流程已经很成熟:模型收到用户问题,判断需要调用哪些工具,输出一个 tool_call 结构化指令,Agent 执行函数,把结果返回给模型,模型生成最终回答。但这个流程每一步都有坑,我挑几个实操中反复踩到的点重点说明。

3.1 声明工具的第一原则:会被模型准确理解

工具声明里包含名字、描述、参数结构,看起来简单,但很多人忽略描述的重要性。模型不是靠猜来判断什么时候调用工具的,它主要读描述。比如一个查询库存的工具,描述如果只写“查询库存”,模型在用户问“货还够吗”时可能拿不准要不要调用。更好的描述是“根据商品 SKU 查询当前可售库存,返回可售数量与仓库位置,适合用户询问货源、库存、备货时调用”。

参数结构也要尽量做细。我一般用 JSON Schema 描述,每个字段写明用途,枚举值要清晰。比如仓库字段如果只会接收“华东仓”或“华南仓”,那就用 enum 限定,避免模型传一个“上海仓库”导致工具层报错。

{ "name": "query_inventory", "description": "根据商品 SKU 查询当前可售库存,返回可售数量和仓库位置,适合用户询问货源、库存、备货时调用", "parameters": { "type": "object", "properties": { "sku": { "type": "string", "description": "商品SKU编码" }, "warehouse": { "type": "string", "enum": ["华东仓", "华南仓"], "description": "期望查询的仓库,不传则默认全部仓库" } }, "required": ["sku"] } }

这里有个经验:工具数量越多,模型选错的概率越高。Hermes Agent 里我会给前端能看到的工具做分组,比如全部工具 20 个,但按业务场景把常用 6 到 8 个先暴露给模型,其余按需加载。模型看到的选择越少,命中率越高。

3.2 tool_call 流程与参数校验

当模型返回 tool_call 后,Agent 不要立即执行,先做校验。模型生成的参数经常有幻觉,比如 sku 可能多位、仓库名字可能传成模糊文本。我会先跑一遍 JSON Schema 校验,如果失败,把错误信息返回给模型让它重写参数,而不是硬着头皮执行。

执行时我还会加一层超时控制。某些外部 API 可能卡住,工具调用如果一直不返回,整个对话就会被卡死。所以每个工具函数都要包一个超时机制,比如 10 秒还没返回就中断,返回一个“查询超时”的结构化结果,让模型基于这个结果回复用户“稍后再试”。

3.3 工具结果回填与二次迭代

工具返回的数据往往很干,比如一个 JSON 数组,里面是商品 ID、数量、价格。如果直接把原始 JSON 塞给模型,模型能懂,但回答质量不一定好。我会在工具层先做一次格式化,把关键字段转成可读语句,比如“SKU A1001 在华东仓有 120 件,华南仓缺货”,减少模型的解析负担。

返回给模型之后,它可能还需要二次调用其他工具。比如查库存之后,用户接着问“那补货周期多久”,模型需要再调一个补货周期工具。Hermes Agent 的执行循环要支持连续 tool_call,也就是模型第一次返回工具调用,执行后再次请求模型继续判断,直到模型认为不需要再调用工具为止。这个循环要有最大迭代数,比如 5 次,防止模型陷入循环调用。

3.4 工具调用避坑经验

我在工具调用上踩过不少坑,挑几个影响最大的说。第一,不要暴露删除类工具给普通会话,除非有严格的权限校验;我见过模型在语义模糊条件下误调用删除操作的案例。第二,工具返回的错误信息要标准化,最好统一成{ "success": false, "error_code": "...", "message": "..." },这样模型才能做出合理的用户安抚话术。

第三,工具执行环境要隔离。如果智能体能执行代码或写文件,务必放在沙盒里,限制网络访问和文件路径,否则一次越权调用可能带来很大麻烦。Adam 的本地工具调用我通常走一个单独的子进程,用户态权限降到最低,绝对不让模型直接拼接 shell 命令。

4. MCP 接入:别把每个接口都硬编码

讲完工具调用,MCP 是绕不开的升级项。我在早前的 Agent 项目里吃过工具维护的苦:每加一个数据库、每接一个第三方服务,都要重新写函数、重新描述参数、重新测试。MCP 帮我大大压缩了这部分重复劳动。

4.1 选择 MCP 的边界

先泼个冷水:不是所有场景都适合上 MCP。如果只是给 Agent 加一两个固定函数,直接写成普通工具反而更轻量。MCP 的价值在于标准化和复用,比如你要同时接文件系统、多个数据库、多个 HTTP API,而且这些能力还要在其他 Agent 项目中重复使用,那上 MCP 就是划算的。

MCP 的优点是“一次描述、多处复用”。你写好一个 MCP server,它可以通过标准协议把自己的工具列表、参数定义、资源列表提供给 Agent 运行时。Hermes Agent 作为客户端,启动时主动发现这些能力,不需要你在 Agent 代码里写死每个函数名。

4.2 MCP server 常用配置结构

Hermes Agent 里配置 MCP server 通常用一份注册文件。下面是一个标准的示例结构:

mcp_servers: internal-search: transport: stdio command: "python" args: ["mcp_search_server.py"] env: INDEX_PATH: "./index" TOP_K: "10" sqlite-db: transport: stdio command: "python" args: ["mcp_sqlite_server.py"] env: DB_PATH: "./app.db"

这里 transport 用的是 stdio,意思是 Agent 启动一个子进程,通过标准输入输出与 MCP server 通信。对本地工具来说这是最稳定的方式,不需要开着额外端口。如果是远程服务,也可以换成 streamable-http,配置里写成服务地址和鉴权 token。

MCP server 启动后,Agent 会触发一次“初始化握手”,获取 server 的能力列表。我在实际开发中会写一个健康检查流程,每次 Agent 启动时打印已经连接的 server 和可用工具数量,防止配置写错了而没发现自己连的是残缺服务。

4.3 工具、资源与提示词模板

MCP 除了提供工具,还有两类能力容易被忽略:资源和提示词模板。

资源适合放“给模型参考的静态或半静态数据”。比如一个项目的文档说明、一套业务规则、一份库存分类表。Agent 可以在适当的时候主动读取资源并加入上下文。这比把所有规则全写进系统提示词要灵活,因为资源可以按需加载,不会白白占 token。

提示词模板则是一种对话模板能力,比如 MCP server 可以提供一个“请假审批助手”的提示词模板,Agent 发现用户意图匹配时加载这套模板,连同相关工具一起使用。这三个能力加在一起,MCP server 就不再只是一个“工具桶”,而是能打包“能力 + 数据 + 使用方式”的整体模块。

4.4 与本地工具并存时的路由优先级

Hermes Agent 里通常有本地工具和 MCP 工具同时存在。这时候要注意路由优先级设计。我的原则是:可以本地完成的优先本地,涉及外部系统或第三方数据的走 MCP。比如“读取本地配置文件”这种操作,本地工具又轻又稳;而“查询某个独立服务的数据”则通过 MCP 统一接入。

具体实现上,我建议在工具描述里加上来源标识,比如 MCP 工具的名字前带上 server 前缀,或者在描述里写清楚“通过 XXX 服务获取数据”。模型看到描述就能判断信息来源,不会在多个相似工具之间乱选。如果本地工具和 MCP 工具功能确实重叠,我会干脆只暴露其中一个,避免选择冲突。

5. 记忆:短期与长期分开管理

记忆是整个 Hermes Agent 里最不容易一次做对的部分。太多人把“记忆”等同于“把历史聊天存起来”,但存起来不等于会被模型正确使用。如果记忆设计得不好,轻则模型找不到关键事实,重则被一堆无关信息干扰判断。

5.1 记忆分层

我自己在项目里会把记忆分成三层:工作记忆、短期事实、长期画像。

工作记忆就是当前会话上下文中的信息,随会话结束自然消减;短期事实是跨会话但仍然比较新的信息,比如用户上周查过的某批次商品编号,时效性强,可能要保留几天到几周;长期画像是用户的基本偏好、稳定属性,比如“用户只关心华东仓现货”“决策风格偏向保守”,这类信息基本不会频繁变化。

Hermes Agent 的记忆模块用一张带 type 字段的存储表就能覆盖这三类。工作记忆存在会话结构中,短期事实和长期画像则写入 vector store 或结构化键值库。每次对话开始时,我会先按用户 ID 拉取长期画像和近期的短期事实,作为系统提示的一部分注入。

5.2 向量化的语义检索

光把记忆写成文本存在数据库里,模型要找到相关记忆会很吃力,因为你无法精确预测用户会用哪种说法来提问。所以我用向量化检索:把记忆切块,每一块生成嵌入向量,用户新消息到来时也生成向量,然后用余弦相似度找出最相关的几条记忆。

嵌入模型的选择,我会优先考虑准确率和部署成本平衡的本地模型,而不是每次检索都调用外部 API。记忆切块也有讲究,大块信息容易丢失细节,小块信息容易语义不完整。我一般按语义段落切分,每块控制在 200 到 500 字之间,相邻块保留少量重叠,避免一句话被截成两半导致检索结果残缺。

5.3 写入时机和召回格式

记忆写入不能太频繁,也不能太迟钝。太频繁会导致库变成一堆噪声,太迟钝又会漏掉关键信息。我总结了一个相对稳定的触发策略:在一次对话结束或意图明显切换时,抽取与用户目标、关键偏好、决策结果相关的事实,写入记忆库。比如用户最终选择了某款商品并强调要现货,我会记下“用户偏好现货商品,对交期敏感”。

召回格式同样重要。Hermes Agent 在每次推理前会把命中的记忆拼成一段“长期记忆摘要”,放回上下文里。例如:

## 长期记忆摘要 - 用户偏好:只查看华东仓现货,可接受起订量 200 件以上 - 近期事项:三天前咨询过 SKU A1001 的补货周期 - 注意点:用户对价格不敏感,对交期敏感

这段摘要放置的位置要在系统提示附近,而不是混在最近聊天记录里。因为模型对“当前任务”和“背景资料”的注意力分配不同,分开区域更容易让它把摘要当作稳定约束。

5.4 记忆优先级与遗忘

记忆如果只写不删,时间一长必定劣化。短期事实过期后应该自动降权或删除,比如用户几周前问的高铁时间查询,放在一个月后基本没有再用价值。我给记忆条目加一个时间衰减因子,每次检索时命中的分数按时间递减;超过一定阈值就归档或删除。

还要处理记忆冲突。比如用户先说过“只要华东仓”,后来又明确说“华南仓也可以”,如果旧记忆还在,模型会看到矛盾信息,回答就会摇摆。我加了一个“最后写入覆盖”机制:同一个维度的事实更新后,旧条目自动标记失效。这样模型读取记忆时不会自相矛盾。

6. 串联起来的实践链路和排查

说完四个能力块,我用一条完整链路的例子把它们串起来,再列一份排查速查表。单独理解每个模块容易,把它组合成能稳定跑业务的系统,才是真正花时间的部分。

6.1 一个端到端示例

假设我接了一个内部商品问答助手,用户提问“华东仓还有哪些商品能现货发货”。完整链路是这样的:会话模块先把当前问题附带用户长期画像注入系统,模型判断需要调用库存查询工具,通过 MCP 工具服务查询数据库;工具返回可用商品列表,模型再把结果整理成自然语言回答;同时记忆模块从这次互动中抽取“用户关注现货发货”“偏好华东仓”两个事实写入长期画像。

下一次用户隔了一天来问“那 A1002 能发吗”,会话模块虽然已经看不到昨日对话,但长期画像已经带上偏好信息,模型直接结合当前查询结果回答,并主动提示华东仓现货情况。这条链路里,会话负责即时信息,工具负责真实数据,MCP 负责统一接入,记忆负责跨天复用,缺一个环节体验都会明显下降。

6.2 常见问题速查

我把实际运行中碰到的典型问题整理成一张表,方便排查时对照。

现象可能原因处理方式
模型明明有工具却不用,直接编答案工具描述太模糊,或不在当前已加载列表重写描述,加入触发场景关键词;减少工具数量
多个工具返回冲突,模型来回横跳工具结果没有约定优先级在工具描述或结果中标注数据来源和时效
上下文在长对话中爆掉没有做滑窗压缩启用在途摘要,收缩窗口保留轮数
MCP server 连接失败却无感知初始化握手失败未被捕获增加启动健康检查,打印已连接服务列表
记忆召回到的内容过时缺少时间衰减与覆盖机制给记忆条目加时间戳,过期降权,同维度覆盖
流式输出卡住,界面转圈工具调用阶段没有发送中间事件工具执行前先推送到前端流式事件

6.3 个人习惯与调优建议

最后说几个我在调试时养成的习惯。我会给 Hermes Agent 的每次推理都打印一份“上下文构成清单”,内容包括系统提示占了多少 token、会话摘要多少、工具描述多少、长期记忆多少。只有看到这些数字,才能知道到底是哪部分把窗口撑爆了,或者哪部分占的权重过重。

我还会把所有工具调用记录独立成日志,包含模型原始 tool_call、校验结果、执行结果、耗时四段。这比只记最终对话更有利于排查问题,因为很多错误的根源藏在工具调用参数里,而不是对话文本里。用这份日志复盘时,我经常能发现模型重复选择同一错误参数,然后通过调整枚举值描述解决掉。

调优的顺序我建议先做会话隔离和上下文收敛,再做工具调用的准确率,最后再优化记忆。很多人一上来就搞向量库和长期记忆,结果连会话上下文都经常串线,基础不牢,上层再炫也没有意义。先把最容易被用户感知的多轮衔接和工具执行稳住,记忆这类能力可以逐步迭代,我记得第一次接入 MCP 是因为旧数据源切入太慢,那时候我先停了十个本地工具,换成一个符合协议的查询服务,整个链路反而更稳定了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进