ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

智能体框架核心设计:会话管理、工具调用、MCP与记忆系统实战

智能体框架核心设计:会话管理、工具调用、MCP与记忆系统实战

近两年做智能体(Agent)项目的人越来越多,但大多数团队都卡在同一个地方:模型能聊天,但干不了实事。要让大模型真正“上手干活”,绕不开会话管理、工具调用、外部协议接入、记忆系统这四根柱子。我最近在做的Hermes Agent项目,正好把这四块能力完整趟了一遍,这里把关键设计和踩过的坑整理出来,给同样在做智能体框架的朋友一份参考。

Hermes Agent是一个面向通用任务执行的智能体运行时框架,核心定位是“让模型不仅能说,还能做”。它通过结构化的会话状态管理、动态工具注册与执行、MCP开放协议接入、分层记忆存储,把大模型从单纯的文本生成器变成能感知上下文、调用外部能力、跨会话保持状态的执行体。这套设计适合正在搭建智能体产品的开发者、研究多智能体协作的团队,以及准备在生产环境中落地Agent应用的工程人员。下面按能力拆开细说。

1. 会话能力拆解:多轮对话状态管理的设计思路

1.1 会话不只是消息列表,而是一个“工作台”

很多初版Agent实现里,会话就是往消息数组里不断append,全量拼进Prompt。这种做法在测试Demo时看不出问题,一旦进入真实业务场景,就会暴露两个致命缺陷:token消耗失控,模型注意力被无关历史稀释。

我在Hermes Agent里把会话重新定义为“结构化工作台”,而不是线性消息流。每个会话包含三类数据:核心消息序列(用户输入、工具结果、模型回复)、衍生状态(当前任务目标、待执行动作列表、引用上下文)、运行指标(轮次计数、Token消耗统计、各工具调用频率)。

这么做的好处是,Prompt组装阶段可以根据当前任务意图动态筛选历史消息。比如用户在执行一个多步骤的数据处理任务时,前三轮关于账号配置的闲聊就不应该继续占据窗口。我在实践中的做法是:给每轮会话打上意图标签,构建阶段只挑选与当前意图标签匹配或时间衰减权重高的历史消息。

1.2 对话中的上下文窗口管理策略

上下文管理直接决定Agent的稳定性和成本。我试过几种窗口策略,从最粗暴的“固定截断最近N条”到“按Token预算动态裁剪”,最后落地的是组合策略:分层保留 + 摘要压缩。

  • 核心层:最近2轮完整消息,保留所有细节,模型推理主要依赖这部分
  • 工作层:当前任务相关的历史消息,按相关度排序保留,单条消息会做截断
  • 归档层:更早的对话内容交给记忆模块处理,会话层仅保留摘要

实现上有一个细节容易忽略:截断操作会在Tools调用结果的返回路径上产生脏数据。工具返回的JSON片段如果不做结构归一化直接塞进历史,下一次模型调用工具时,参数里可能出现残留字段。我的处理是在会话存储层加一个标准化器:工具结果统一转为JSON Schema定义的结构,无法解析的原始输出降级为纯文本摘要。

1.3 会话恢复与Branch处理

另一个会话层面的硬骨头是恢复和分支。用户在调试Agent时会反复执行同一个任务,而上一轮的中间状态可能已经失效。Hermes Agent在会话记录里增加了branch标记:每次工具调用或用户消息变更时生成新的分支节点,回滚时只需切换branch指针,不需要重建整个上下文。

// 会话分支切换的简化实现思路 class SessionBranchManager { constructor(sessionId) { this.branches = loadBranches(sessionId); this.currentBranch = this.branches[this.branches.length - 1]; } rollbackTo(branchId) { const target = this.branches.find(b => b.id === branchId); if (!target) throw new Error(`Branch ${branchId} not found`); this.currentBranch = target; rebuildPromptContext(target); } forkFrom(branchId) { const base = this.branches.find(b => b.id === branchId); const newBranch = createChildBranch(base); this.branches.push(newBranch); this.currentBranch = newBranch; return newBranch.id; } }

这个设计在支持“重新执行上次失败步骤”的场景中特别实用,用户可以先尝试方案A,失败后回滚再走方案B,整个过程模型不会因为历史冲突产生幻觉。

2. 工具调用机制:Function Calling与执行沙箱

2.1 工具定义要像接口文档一样严谨

工具调用是Agent落地中最需要较真的环节。模型对工具的理解完全依赖你提供的定义文本。我刚开始写工具描述时比较随意,用自然语言习惯描述功能,结果模型频繁选错工具。后来总结出工具定义的几个关键字段:

字段作用常见错误
name工具唯一标识,需符合小驼峰命名命名风格不一致,误导模型推断
description说明工具功能的完整描述过于简略或含糊,如“处理数据”
parametersJSON Schema定义参数结构缺少必填项约束或类型定义模糊
restrictions工具使用的边界条件未说明错误输入时的兜底逻辑

Description字段的写法直接影响模型选工具的准确率。经测试,一个好的描述应该包含三个维度:功能范围、典型使用场景、与其他工具的边界。例如“get_user_orders”的描述不能只写“获取用户订单”,而要写“根据用户ID查询其在当前平台的全部订单订单状态包含pending、paid、shipped、completed。此工具仅用于查询订单不处理退款相关操作”。

2.2 工具执行的隔离与权限边界

模型生成的工具调用参数本质上不可完全信任。一个正规的Agent框架必须在执行层做隔离。Hermes Agent的工具执行统一走子进程或运行时沙箱,不在主进程内直接执行模型传来的代码或命令。

我在框架中抽象了一个执行器接口,支持三种执行后端的切换:本地子进程(适合开发调试)、容器运行时(适合多租户隔离)、远程函数计算(适合生产扩展)。切换的关键参数是资源限制和超时控制。检测模型调用工具时,必须能优雅中断超过策略阈值的执行。超时阈值我一般设置为普通工具10秒、重型任务30秒,不同的工具类型可以在注册时单独覆盖。

另外还需要注意工具执行的幂等性。LLM在生成失败重试时,很可能会把同一个写操作请求发出两次。框架在处理工具调用时需要记录request_id,同一个request_id重复到达时直接返回上一次的执行结果,而不是再次执行副作用操作。这个点在对接支付回调、文件写入等场景中极其重要。

2.3 工具结果反馈的结构化压缩

工具执行完成后返回的结果,是整个链路中最容易失控的环节。一个查询工具可能返回数百行JSON,直接全量塞进上下文,Token瞬间爆炸,而且模型也抓不住重点。

我采用的方法是“三级结果处理管线”:

  • 第一级:原始结果按Schema校验,非法结构过滤
  • 第二级:超长字段截断,数组超过20条时抽样并标注总数
  • 第三级:根据当前任务目标生成摘要字段,摘要由模型在空闲轮次预生成,还是由规则模板即时生成,取决于结果复杂度

规则模板适合结构化强、字段固定的工具结果。例如数据库查询结果表结构固定,可以直接用模板抽取列名和行数。而网页抓取这种非结构化结果,需要额外调一次模型生成摘要,虽然增加一次推理开销,但后续对话质量的提升非常明显。

3. MCP集成方法:扩展能力边界的关键协议

3.1 MCP在Agent框架中的定位

MCP(Model Context Protocol)可以理解为智能体世界的USB-C接口。过去每个Agent要接外部数据源,都要写一套自定义对接逻辑,数据源一变代码就得跟着改。MCP的初衷是把“工具提供方”和“工具消费方”解耦,统一成一套标准协议。

在实际架构中,MCP解决的是“Agent能感知和操作什么”的问题。HerMes Agent通过MCP客户端连接文本编辑器、数据库、浏览器自动化工具、企业IM等外部能力。每次连接对应一个MCP Server实例,Server暴露的工具以统一格式向Agent注册。Agent侧只需要维护一份Server注册表,不需要关心具体服务是Python写的还是Node.js写的。

3.2 协议对接的踩坑实录

MCP的对接过程并不是文档里写的那么顺畅。最典型的坑是初始化握手和Capability协商。不同语言版本的MCP SDK对工具列表的返回格式存在细节差异,有些版本返回的schema直接兼容,有些则需要手动做一层转换。

标准化的对接流程我拆成四步:

  1. 启动外部服务进程,通过stdio或SSE建立通道连接
  2. 发送initialize请求,携带协议版本和客户端能力声明
  3. 等待服务端返回初始化响应,记录服务端支持的工具列表
  4. 注册工具回调,将MCP工具翻译为Agent内部统一的工具Schema
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": { "tools": { "listChanged": true }, "resources": { "subscribe": true } }, "clientInfo": { "name": "hermes-agent", "version": "0.4.2" } } }

实操中容易忽略的点是服务端进程的优雅退出。Agent退出时如果没有向MCP Server发送shutdown通知并等待其清理资源,会导致下次启动时的端口占用和缓存脏数据。我给框架的MCP客户端加了生命周期钩子,在Agent主进程退出前统一执行关闭序列。

3.3 连接池与多Server调度

一个Agent同时接多个MCP Server时,调度策略就变得很关键。大多数MCP Server单进程承载不了高并发请求,所以不能每来一次工具调用就新建连接。我在Server连接层加了连接池配置,每个Server默认维持min=2, max=10的动态连接数。

调度时还有一个优先级问题:多个Server提供类似能力时,模型该选哪一个?我给每个Server配了权重评分,由三个维度构成:响应延迟的滑动平均、工具调用历史成功率、服务端声明的能力匹配度。这个评分会在实际调用后实时更新,模型在参考列表中看到的结果就是这个动态评分排序后的版本。

# MCP Server调度权重计算的伪代码 def update_server_score(server_id, latency, success): server = registry[server_id] server.latency_ema = 0.7 * server.latency_ema + 0.3 * latency server.success_rate = 0.8 * server.success_rate + 0.2 * success server.score = ( 0.4 * (1.0 / (server.latency_ema + 0.1)) + 0.6 * server.success_rate )

这套调度机制上线后,多Server接入时的工具调用成功率从85%提升到96%左右。核心收益是避免了“A服务超时了还反复试A”的笨重逻辑。

4. 记忆系统设计:短期与长期记忆的配合

4.1 记忆分成三层:工作记忆、情景记忆、语义记忆

记忆是Agent从“聊完就忘”进化为“越用越懂你”的关键,也是最难做对的部分。记忆系统设计参照认知科学的分类,分成三层。

工作记忆对应当前会话内的临时信息,类似人的“脑内缓存”,比如用户刚提的临时偏好“这次用简洁风格回复”,这一层在会话结束后清空。情景记忆存储具体的交互事件,如“用户在2025年1月10日运行了数据清洗任务,使用了Python脚本”。语义记忆提取的是跨会话的稳定知识,如“用户偏好省中型SQL风格”“用户的操作习惯是先预览后执行”。

三层记忆的存储介质完全不同。工作记忆在内存中,使用TTL淘汰策略。情景记忆写入向量数据库,按时间衰减调整检索权重。语义记忆使用知识图谱结构存储实体与关系,支持推理查询。架构上分层的核心原则是不同生命周期和结构的数据不能混在一个存储里,否则检索质量和维护成本都会失控。

4.2 记忆写入与提取的触发策略

不会写记忆和不会提取记忆都是问题。写入过频则上下文和存储不堪重负,写入过少则Agent丧失长期连续性。我采用的策略是“关键节点强制写入+定期扫描提炼”。

强制写入触发点包括:任务完成、用户明确表达偏好、工具执行失败并重试成功、跨会话引用需求发生。定期扫描则是一个后台任务,每30分钟扫描近期交互日志,用摘要模型抽取候选语义记忆,经过置信度过滤后写入知识图谱。

// 记忆写入的触发判断逻辑(简化版) const triggers = { TASK_COMPLETED: (ctx) => ctx.status === 'success', USER_PREFERENCE: (ctx) => ctx.message.includes('喜欢') || ctx.message.includes('不要') || ctx.message.includes('以后'), ERROR_RETRY: (ctx) => ctx.retryCount > 0 && ctx.status === 'success' }; function shouldWriteMemory(sessionEvent) { return Object.values(triggers).some(trigger => trigger(sessionEvent)); }

提取记忆时,需要注意检索相关性和时效性的平衡。纯向量相似度检索容易把久远的相似事件也翻出来,而且提取结果没有区分直接证据和推测信息。我的做法是混合检索加权重排序:既有向量库相似度打分,也按时间衰减系数加权,最后再加一步记忆类型的过滤器,与当前任务类型不匹配的记忆直接剔除。

4.3 跨会话知识更新的冲突消解

记忆系统最隐蔽的坑是知识冲突。用户可能一开始说“报表喜欢Excel格式”,两个月后说“以后都用PDF”。如果不做冲突消解,Agent会陷入自我矛盾。处理方式是给每条语义记忆绑定置信度和时间戳,新写入的知识如果与旧知识冲突,不直接覆盖,而是同时保留并标记待确认状态,下一次交互根据用户的实际行为裁定胜负。

5. 四种能力的协同编排:一个完整任务的执行流

5.1 从用户请求到最终执行的全链路

单独理解每一种能力还不够,四者协同才能真正体现Agent的价值。我用一个真实场景串联:用户说“帮我把上个月销售数据汇总一下,出一份分析报告,发到项目群,以后每个月都提醒我”。

完整执行链路如下:

  1. 会话模块先激活该用户的工作空间,加载用户画像摘要,判断这是一个跨会话任务
  2. Agent解析意图后,从语义记忆中检索到“用户偏好Markdown格式报告”“汇报对象是销售组”等知识
  3. 工具调用环节:Agent选择 query_database 工具读取销售表,调用 report_generator 工具生成报告初稿
  4. 在生成分析结论时,Agent调用MCP协议接入的网页搜索服务补充行业趋势数据
  5. 工具执行结果经过结构化压缩,反馈给模型做二次总结
  6. 会话模块将本轮的完整过程和结果存档,提炼新的语义记忆“用户每月需要销售汇总报告”
  7. 情景记忆模块记录任务执行细节:报告生成路径、数据表位置、使用的工具列表

5.2 协同时的冲突处理

四能力协同也带来了新的冲突场景:会话需要控制token预算,但记忆系统希望注入更多历史上下文;工具调用追求快速执行,但MCP连接池需要对slow请求进行超时保护。

这些冲突没有银弹解决方案,只能做优先级分级。我在Hermes Agent里定义了“执行优先级”概念,从高到低依次是:用户明确指令 > 工具执行的正确性 > 会话的连贯性 > 记忆的完整性。任何设计决策与高优先级冲突时,低优先级自动让位。比如记忆写入失败时,不阻塞主流程,降级为离线重试队列。

5.3 失败恢复机制

协同链路中任何一个环节失败,都会产生连锁反应。我重点处理两个场景。

第一个是工具执行失败后如何决定下一步。Hermes Agent的判断逻辑是:先检查失败类型。参数校验失败则重新生成参数;服务端崩溃则切换备用的MCP Server;数据缺失则向用户确认输入。判断逻辑必须预设重试次数上限,避免模型陷入死循环。

第二个是会话上下文损坏后的恢复。会话存储使用双写机制,一份是结构化JSON,一份是纯文本镜像。JSON解析失败时用纯文本镜像重建上下文,虽然损失部分结构信息,但至少能维持对话连续性。

6. 常见问题与调试心得实录

6.1 模型连续调用工具的循环失控

最早遇到的高频问题是模型会在一次任务中连续调用同一个工具超过十次不收敛。通常原因是缺乏中间反馈:模型的每次调用都能拿到数据,但没有判断“这些数据是否满足最终目标”的过程。解决办法是在每一轮工具执行后追加一段“目标达成度评估”,由规则引擎判定当前状态与最终目标的距离,距离未缩短时强制终止当前策略并提示模型换方案。

6.2 上下文注入顺序对模型输出的影响

Prompt的组装顺序确实影响模型行为。经验总结是最佳顺序是:系统指令 > 用户目标 > 关键记忆 > 相关历史 > 工具结果 > 当前问题。把工具结果放在靠近当前问题的位置能显著提升模型引用数据的准确性。大幅偏离这个顺序时,模型会更容易忽略早期注入的信息。

6.3 工具Schema频繁变更的兼容策略

工具定义不是一成不变的。数据库加了一个字段、接口多了一个参数,工具Schema就需要更新。更新不及时会导致模型按旧参数格式调用,被服务端拒绝。我维护了一组Rule集,每次Schema调整时会根据变更类型自动判断,是否触发缓存过期、是否需要重写历史会话中的相关消息。复杂字段重构时还会额外生成迁移说明注入系统提示,让模型知道新旧接口的异同。

6.4 记忆污染与信息过时

记忆系统运行久了,必然积累过时信息。这是不可回避的问题。我做了三层防御:写入前增加置信度门槛;写入后绑定有效期(情景记忆默认30天、语义记忆默认180天);定期巡检时对低频访问的知识做降权或归档。就算这样也无法完全避免污染,因此所有记忆内容在注入Prompt时都会携带置信度和时间标签,让模型对不确定性信息保持警觉。

7. Hermes Agent的适用范围与边界

7.1 适合什么场景

这套架构适合需求清晰、执行路径可验证的业务场景。例如数据分析Agent、客服工单处理Agent、跨系统数据同步Agent。这类场景的共同特征是外部系统有明确接口,工具调用能产生确定性结果,用户有明确的目标预期,并且流程中可以设定成功率、时延等量化评估指标。

7.2 不建议硬套的场景

不要指望Agent框架解决所有问题。纯开放式的创意生成场景不需要复杂的工具调用,过重的会话状态管理反而拖慢响应。实时性要求极高、达到毫秒级延迟的场景也不适合,多轮推理和工具调用的耗时在这里是硬伤。此外,高风险决策场景如果缺少人工复核环节,再强的记忆和工具能力都不该直接拍板。

7.3 扩展方向:多智能体协作

Hermes Agent目前的单Agent能力已经相对完整,我正在扩展的方向是多智能体协作。每个Agent实例独立运行,通过共享记忆层和MCP协议互相感知。当一个Agent发现自身能力边界时,可以向协作网络广播任务,由匹配到的其他Agent接管子任务。这套扩展依赖的核心还是本文说的四种基础能力,只是增加了Agent之间的会话路由和记忆共享协议,后续有实际进展了我再写一篇细聊。

回到这篇文章的主题,我想强调的是:会话、工具、MCP、记忆不是四个独立模块,而是一个完整执行体的一体四面。会话是工作台,工具是手,MCP是外部世界的接口,记忆是长期经验库。四者配合得好,Agent才能真正从一个聊天窗口进化为一个数字员工。我做Hermes Agent过程中的一个体会是:不要一开始就追求功能数量,先把会话状态管理做扎实,再铺工具和记忆,迭代节奏会更稳。

RELATED READING

延伸阅读

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