资讯动态

Agent框架四大核心能力拆解:会话、工具、MCP与记忆的实践

发布时间:2026/10/22 10:34:59 来源:尧图企业网站定制
1. 为什么Agent框架绕不开会话、工具、MCP、记忆这四件事我一直觉得一个Agent框架能不能从玩具Demo变成能干活的东西不取决于它接了多少个大模型API而取决于四个基础能力有没有真正做实会话Conversation、工具Tool、MCP接入、记忆Memory。这句话听起来像正确的废话但真去做的时候你会发现绝大部分翻车案例都出在这四件事的边界没划清楚。我维护的Hermes Agent项目一开始也只是个套壳聊天方案用户发一句话框架把聊天记录拼一拼丢给模型模型回一句完事。后来接工具调用的时候开始出问题模型返回的function_call参数经常多一个字段、少一个字段或者参数类型不对再后来接入MCP服务器发现工具名和参数名跟本地注册的工具冲突等你想让Agent记住用户上个星期说过什么偏好时会话里的历史消息早就被截断得干干净净。这四个问题单独看都不难合在一起就变成了一个复杂的系统工程问题。这篇文章不聊概念只聊落地。我会围绕Hermes Agent里这四个能力的实际设计取舍和踩坑过程展开。会话怎么管理上下文又不失控工具怎么定义才能稳MCP到底怎么接入才不算重复造轮子记忆方案怎么分层才扛得住多轮真实使用。看完你可以直接照着这个思路去设计自己的Agent框架或者拿Hermes的实践经验做参照。先说结论性的判断会话是框架的骨架工具是Agent的双手MCP是工具生态的标准化插槽记忆是让Agent从一次性问答变成越用越懂你的关键。四个能力不是独立的模块而是互相咬合的齿轮。下面我按实际开发顺序一个个拆。2. 会话层设计不是存聊天记录这么简单2.1 会话状态的生命周期管理Hermes Agent的会话层最开始就是一张简单的表session_id、user_id、messagesJSON数组、updated_at。够用是够用但很快暴露问题——当时给一个模拟客户项目某客服系统Demo接入发现同一个用户多开窗口聊天时每个窗口都在往同一个session_id里追加消息上下文互相污染。排查下来才发现做前端联调的时候大家图方便直接用了user_id当session_id多人共用一台测试机时全乱套。正确的做法是给每次会话分配独立ID而且区分两个维度会话身份和会话状态。会话身份是稳定的比如用户ID 会话标题决定这个会话属于谁会话状态是易变的当前上下文窗口、剩余token量、最后活跃时间决定这次对话能不能继续。Hermes现在的做法是class Session: def __init__(self, session_id, user_id, title): self.session_id session_id self.user_id user_id self.title title self.messages [] # 消息瞬时长文存储 self.context_metadata {} # 上下文裁剪信息、压缩摘要等 self.created_at datetime.now() self.updated_at datetime.now()所有消息都走append_message()接口写入外部代码禁止直接改messages列表。这个约束很重要因为后续要做上下文裁剪、摘要压缩、消息回放都必须知道消息的完整轨迹而不是只知道最后几条。2.2 上下文窗口管理裁剪策略比模型参数更影响体验大模型的上下文窗口是有限的哪怕支持200K token的模型也不能无脑把聊天记录全塞进去。一次真实的Agent对话中每条工具调用的输入输出都很占token——一个普通API返回的JSON可能就有2000 token对话超过十轮之后历史消息量就非常可观了。如果不做管理最直接的后果是请求超时、成本爆炸、模型开始忽略中间信息。我在Hermes里做了两级裁剪策略第一级滑动窗口裁剪。保留最近的N轮对话我默认设12轮完整消息包括user/assistant/tool三类消息更早的压缩成摘要放进一个特殊的summary消息里。这样即使会话持续了一整天模型每次看到的上下文结构都是历史摘要 最近12轮完整内容既有连续性又有细节。第二级关键信息抽离。有些信息是绝对不能丢的比如用户明确说过的身份信息、偏好、约定。这类信息在写入会话时就会打上sticky标记裁剪时跳过。具体的记忆持久化放到后面记忆章节讲这一层只做会话内的留存。这套方案实际跑下来很稳。有个实测数据可以参考没做裁剪前一个用了40分钟的中等复杂会话每次请求要发约2.3万token做裁剪加摘要压缩之后降到6000 token左右响应速度提升明显模型也再没出现过忘记前面结论的情况。2.3 消息结构设计要为工具调用服务会话层最容易忽略的设计点是消息结构必须考虑工具调用的完整性。OpenAI系的API里有个经典规则tool_calls和tool_message必须成对出现中间不能插别的消息否则接口直接报错。Hermes在消息模型里加了kind字段区分消息类型class Message(BaseModel): role: str # user | assistant | tool | system kind: str # text | tool_call | tool_result | summary content: str tool_call_id: Optional[str] None tool_name: Optional[str] None sticky: bool False做会话回放时框架会按规则重新组织消息序列如果检测到tool_call后面没有紧跟着对应的tool_result就说明上下文被裁剪破坏了工具调用对这时候要主动补一条空结果或说明消息而不是直接把序列发给模型。这个Bug我踩过一次裁剪后模型返回了一个诡异的回答像是把工具返回内容当成了用户语音指令。从那以后裁剪逻辑里专门加了这个完整性校验。3. 工具层Function Calling的正确打开方式3.1 工具注册Schema先行还是函数先行工具层最核心的设计决策是工具定义的第一公民是什么Hermes早期用的是函数先行也就是你写一个Python函数框架用inspect.signature去解析参数自动生成schema。开发快是真快但出了问题也是真难查——嵌套的dataclass参数、类型为Optional[List[str]]的可空列表、带默认值的枚举参数这些类型inspect解析出来的结果跟模型真正需要的JSON Schema经常对不上。后来我改成Schema先行每个工具必须显式声明自己的输入输出结构函数本体只是该Schema的实现agent.register_tool( nameget_weather, description查询指定城市的实时天气信息, input_schema{ type: object, properties: { city: {type: string, description: 城市中文名如 北京}, date: {type: string, description: 日期格式YYYY-MM-DD默认今天} }, required: [city] } ) def get_weather(city: str, date: str None) - dict: # 实际实现 return {temperature: 26, condition: 晴}虽然后期增加了一些类型推导工具来辅助编写Schema但核心原则不变工具对外暴露的契约必须由开发者显式定义而不是从函数签名里猜。这个转变花了一个下午但之后工具层的Bug率直线下降因为Schema和函数签名不一致的问题从根源上消失了。3.2 参数校验与错误注入模型也会给错参数模型调用工具时给错参数不是偶发事件而是常态。我统计过一个周期的数据本地模型平均10次工具调用里有2次参数不符合Schema主要问题集中在缺少必填字段、字段类型不对字符串传成数字、枚举值超出定义范围。如果不做校验直接执行要么函数内部报出难以理解的Traceback要么更糟——函数返回了一个错误码但模型不知道这个错误码是什么意思。Hermes的解决办法是在执行工具前做严格校验校验失败时不直接放弃而是把错误信息转换成工具结果回送给模型让模型自己纠错def execute_tool(name, args, tool_registry): tool tool_registry.get(name) try: validated validate_json_schema(tool.input_schema, args) result tool.func(**validated) return {success: True, output: result} except SchemaValidationError as e: error_msg f参数校验失败: {e.message}请修正参数后重试 return {success: False, error: error_msg}注意这个返回结构不是抛异常而是正常返回给模型。模型看到参数校验失败之后大部分情况会重新组织参数再调一次少数情况会向用户解释说自己无法完成。这个让模型自己纠错的设计让工具调用的整体成功率从82%提升到了94%以上。3.3 超时控制和幂等策略分布式思维要前置工具执行不是本地的return那么单纯。很多工具要调外部HTTP API网络超时、上游5xx、返回格式变化都很常见。我在工具层加了三类基础能力超时控制默认10秒可以在注册时覆盖、重试机制针对网络抖动最多重试2次指数退避、幂等保护对用户侧可能重复触发的工具如支付创建、发送短信要求工具注册时声明idempotent属性并在调用时检查是否已有相同幂等键的执行记录。这三件事做在前面后面接MCP的时候省了非常多事。因为MCP服务器本质上就是一堆远程工具的聚合体如果工具层本身不具备容错能力MCP服务器的任何一次抖动都会直接传导到Agent的主流程。4. MCP接入工具生态的标准化插槽4.1 为什么选择MCP而不是自己定义工具协议选型的时候组里有同事提过一个问题既然我们已经有了自定义工具注册机制为什么还要引入MCP这不是重复造轮子吗我的判断是工具注册机制解决的是Agent内部如何描述工具MCP解决的是不同的Agent框架和不同的工具服务器之间如何互联。前者是内政后者是外交。假设我们做了一个非常好用的本地文件检索工具如果没有MCP其他团队想接入就得扒我们的源码看工具协议文档然后写适配层如果走MCP我们只需要把工具包成MCP服务器对方用标准客户端就能直接发现和调用。实际开发中我建议切成两层工具层Tool LayerAgent内部的工具描述、参数校验、执行上下文。MCP适配层MCP Adapter把MCP服务器暴露的工具翻译成Agent内部的工具描述。这样设计之后工具层不直接依赖任何MCP的SDK。以后如果出现更优的协议适配层替换掉就行工具层纹丝不动。4.2 实现一个轻量MCP客户端的核心流程Hermes里没有引入很重的MCP框架而是基于JSON-RPC 2.0自己实现了一个轻量客户端。核心流程只有四步发现List Tools发送tools/list请求拿到远程工具名、描述、输入Schema。校验Schema Check把远程工具的Schema映射成Agent内部的input_schema格式做兼容转换。调用Call Tool发送tools/call请求带name和arguments拿到工具结果。处理结果Result Normalization把MCP返回的content数组规范成统一的{success: bool, output: any}格式。class MCPAdapter: def __init__(self, transport): self.transport transport # stdio 或 streamable HTTP def list_tools(self): resp self.transport.send({jsonrpc: 2.0, method: tools/list, id: 1}) return [tool for tool in resp[result][tools]] def call_tool(self, name, arguments): resp self.transport.send({ jsonrpc: 2.0, method: tools/call, params: {name: name, arguments: arguments}, id: 2 }) content resp[result][content] text_parts [item[text] for item in content if item[type] text] return {success: True, output: \n.join(text_parts)}4.3 MCP的坑标准协议的语义鸿沟MCP协议本身定义得很清楚但实际部署时坑很多。最大的坑是工具名冲突和语义不一致。工具名冲突很好理解两个不同的MCP服务器一个提供send_email另一个也提供send_email但参数结构完全不同。Hermes的解决办法是给每个接入的MCP服务器加命名空间前缀比如github_*、jira_*前缀注册时由集成方显式指定。语义不一致是更隐蔽的问题。MCP的resources和prompts两个原语本意是让服务器暴露数据文件和提示词模板。但很多MCP服务器实现得比较粗糙把工具调用透传成了资源读取返回的结构里塞了一堆元数据。我用的时候发现resources/read返回的数据经常是一个大JSON但模型很难从里面提取关键信息。后来固定处理方式凡是MCP返回的文本内容都先做一次文本清洗和摘要提取再进上下文避免原始噪声数据污染模型的判断。还有一个很实际的坑MCP传输方式的选择。stdio适合本地进程注入但如果你是远程部署的Agent服务器stdio客户端跑不起来必须用HTTPStreamable HTTP。我建议新项目直接上HTTP传输少一层进程管理的麻烦。5. 记忆机制从一次性问答到越用越懂你5.1 记忆分层的通用模型工作记忆、情节记忆、语义记忆记忆是这四个能力里最容易被做花哨、也最容易被做废的。我看到很多Agent项目一上来就接向量数据库把所有聊天记录全向量化然后每次对话开始前把所有相似的记忆都塞给模型。结果是上下文爆炸模型被一堆不相关的记忆干扰效果比不接记忆还差。Hermes的实践方案是三层记忆模型记忆层级存储介质生命周期典型内容工作记忆会话内单次会话最近12轮消息、当前任务状态情节记忆结构化存储数小时到数天某次会话中的关键事件、用户明确提出的偏好语义记忆向量库数周到永久跨会话的长期偏好、常用配置、事实性知识这个分层的核心逻辑是不是所有东西都值得长期记忆。用户在聊天过程中说的一句话也许只是当下的随口一提如果被当成长期偏好记住了后面Agent可能会在错误的场景下调用这条记忆造成很不好的体验。我吃过这个亏有一版实现把所有用户提到过的城市都记成了偏好结果一个用户只是聊天时提到去过上海之后每次推荐都推荐上海的东西用户直接投诉。5.2 记忆的写入策略显式提取 隐式积累相结合Hermes的记忆写入分两条路径显式提取Explicit Extraction每一轮对话结束时跑一个小模型专门提取可记忆信息用固定的JSON格式输出{ memories: [ {type: preference, content: 用户偏好使用简洁回复风格}, {type: fact, content: 用户在某某平台上的商家ID是M-23891} ] }提取出来的信息经过置信度打分高于0.8的写入情节记忆或语义记忆低于阈值的丢弃。隐式积累Implicit Accumulation用户在某些固定场景下的行为规律比如用户每次问天气都要追问那明天呢Agent会在行为记录里累加频次当频次超过3次时自动生成一条用户习惯连续追问多天天气的记忆。这点设计后来被证明非常有用因为它能捕捉到用户自己都没有意识到的使用习惯。这两条路径我分别设置了开关和权重。显式提取的开关默认开隐式积累的开关默认关因为需要观察期不是所有场景都适用。5.3 召回策略相关性和时效性的双重约束只讲写入不讲召回记忆系统等于没做。Hermes的召回逻辑是当新的用户消息进来时框架会从语义记忆库里检索相关的记忆条目召回条件有三条语义相似度要超过阈值默认0.45可以调最近30天内更新过太久远的记忆除非是事实性知识否则不召回单次最多召回5条防止上下文被记忆条目淹没。召回的条目会以system消息或特殊前缀的形式插入到上下文的最前面告诉模型以下是与当前对话可能相关的历史记忆供参考但不一定都适用于本次对话请自行判断。这个自行判断很关键它给了模型主动权而不是强制模型必须使用记忆。5.4 记忆的联动与失效清洁工很重要有了记忆之后维护就成了新问题。Hermes写了一个周期性的记忆清理任务做三件事合并重复把语义向量距离非常近的记忆条目合并成一条综合记忆降级/删除长期未被召回的语义记忆先降级到情节记忆再过一段时间若仍未被用到就直接删除更新冲突如果新提取的记忆和旧记忆冲突比如之前用户说喜欢A现在说改成B以新记忆为准并给旧记忆打上已过期标签。这套维护机制让记忆库的体积一直保持在一个可控范围内。我之前看到过一个项目因为没做清理向量库里堆了几十万条记忆每次召回都要做全量检索慢得不忍直视。记忆维护这件事必须从一开始就纳入设计而不是等到量大了再补。6. 四者联动打通一次真实任务的链路复盘6.1 端到端流程从用户消息到动作执行单独看每个能力都清楚真正的挑战在联动。以Hermes里的一个实际功能为例子——用户跟Agent说我上次提到的那家川菜馆帮我订个明天晚上7点两个人的位置。这条消息到了任务链路后四个模块的协作过程是这样的会话层先把这条消息追加进当前会话判断上下文窗口是否超限如果超了就触发旧消息摘要压缩。记忆层对新消息做两件事检索上次提到的川菜馆相关的语义记忆同时提取本次新信息用户提到聚餐人数2人、时间偏好。Agent调度层把上下文拼装完发给模型。模型识别出意图生成了工具调用先调search_restaurant工具找到目标餐厅再调make_reservation工具订位。工具层执行第一个工具发现参数缺少餐厅名但从记忆召回的条目里恰好有用户上次提到的餐厅名把参数补全再次执行。MCP适配层发现make_reservation工具实际挂在某个第三方本地生活MCP服务器上转发调用。结果回到会话层模型以自然语言回复用户同时触发记忆层把用户经常使用这家餐厅平台的隐式行为频率加1。这个链路看起来流畅但每一步都有值得注意的细节。记忆补全参数这个行为是让我印象最深的一个设计亮点——它让Agent真正有了上下文联想的能力而不只是机械地执行指令。6.2 联动时最容易出的三类问题及排查思路第一类记忆污染工具调用。第一次做联动时召回的旧记忆不小心覆盖了用户本次明确指定的新参数导致Agent订错位置的严重程度。排查后发现是记忆召回优先级高于用户当前消息的显式参数。修复方案很简单当前消息中的显式信息永远优先于记忆召回的信息记忆只用来填空不允许覆盖。第二类MCP返回结果进入记忆造成垃圾积累。MCP工具返回的原始结果如果被拆分成多条记忆写入语义库结果就是满库都是接口返回的JSON片段。解决方法是标记工具返回内容的记忆写入黑名单只在特定的几个场景比如用户明确说记住这个优惠券码才从工具结果中提取记忆。第三类会话裁剪导致工具调用链断裂。这个问题前面提过多轮工具调用后旧的消息被裁剪掉模型看不到前面已经加了某件商品会重复加购。排查后我在会话层加了完整性校验同时在Agent调度逻辑里维护一个当前任务执行状态的摘要确保裁剪后任务状态不丢。6.3 性能观测每次请求的Token消耗构成联动跑通后我对一次典型对话的Token消耗做了拆解。以3000字左右的对话为例系统指令与工具Schema占比约32%历史消息与摘要占比约38%记忆召回占比约6%当前用户消息与回复约24%。这个比例验证了之前的设计——记忆只占很小的比例不是越多的记忆越好工具Schema占了不少这意味着工具多了之后Token成本显著上升当注册工具超过20个就要考虑工具分组或动态注入而不是一股脑全塞给模型。Hermes后续规划里有一个点我一直想优化更细粒度的工具分组让Agent先根据用户意图预判需要哪一组的工具再动态加载Schema。目前这个功能还在实验阶段但方向很明确。7. 写在最后四个能力的优先级判断为了写这篇复盘我重新审视了Hermes的开发历程如果让我重新来一遍优先级判断会是这样的第一优先会话层。这是Agent的地基所有交互、状态、上下文管理都在这里发生。会话层的消息结构设计一定要把工具调用、记忆注入、摘要压缩都考虑进去否则后期返工成本极高。第二优先工具层。Agent说到底是对话驱动的执行器没有稳定可靠的工具调用会话再流畅也只是个聊天框。Schema先行、参数校验、错误回传这三件事是工具层的底线。第三优先MCP适配层。MCP的引入带来的收益是生态互通但它不是Agent的核心能力。如果团队人力紧张可以先用自己的工具机制跑起来后面再补MCP适配不影响主流程。第四优先记忆。记忆是锦上添花、却也是最容易做过头的能力。先做显式提取做小规模验证再上向量库和语义召回不要一上来就上全量记忆。回到前面那句话记忆不是越多越好而是越准越好。最后分享一个经验这四个能力加在一起真正打磨的时间远比你想象的长。我最初以为两周能把记忆和MCP接完实际花了两个月——大量时间不是花在功能实现上而是花在边缘情况处理和联动Bug修复上。如果你正在做类似的Agent框架不妨先把地基打扎实再谈锦上添花。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑