1. 整体设计与架构思路
如果你也做过一段时间的Agent,应该会有一个同感:单轮对话里的AI看起来很聪明,但一旦把场景拉长,让它在任务中自行调用外部系统、处理多轮反馈,问题就开始露头。我最近在项目代号Hermes Agent的框架上做了一轮重构,核心思路是把能力收敛成四根支柱:会话、工具、MCP、记忆。这篇文章不写泛泛的架构稿,只把我实际验证过的设计、参数和坑完整记录下来,适合正在搭Agent原型、又不想一开始就被各种概念绕晕的开发者。
先看一段使用场景,你就有代入感了。假设你让Hermes做这样一件事:“把上周五的会议纪要从项目目录里找出来,整理成邮件给合作方,并附上我上次提过的附件偏好。”这句话落到四个能力上分别是:会话负责把这句话拆成意图,并维持后续多轮交互在同一个任务流程里;工具负责触发检索文件、生成邮件草稿这些具体动作;MCP负责用标准协议去读目录、调用邮件发送服务;记忆负责把“上次提过的附件偏好”找回来并用上。
同一个任务,去掉其中任何一块都会立刻出问题。没有会话,任务一开始就会被拆散;没有工具,Agent只会回复你“建议去查一下目录”;没有MCP,读取项目目录和发邮件就要写两套完全不同的接口代码;没有记忆,用户去年说过的偏好会在新的一年里彻底消失。
1.1 四个能力在Hermes中的分工
Hermes把这四块能力拆成了彼此独立的模块,但又让它们按固定顺序协同工作。会话模块是整个Agent的主干,它的职责是维护消息列表、判断当前轮到谁发言、控制任务循环什么时候结束;工具模块是主干上的出口,模型说“我想查订单”,工具模块负责找到对应函数并执行;MCP模块更像一组标准集装箱,容器里装着文件系统、数据库或第三方服务,统一通过协议暴露给工具模块调用;记忆模块独立在会话之外,像一个外置档案柜,按需要把历史信息塞回会话里。
| 能力 | 解决的核心问题 | 缺少时的表现 |
|---|---|---|
| 会话 | 让Agent理解“你说了什么、想干什么”,并维持多轮任务目标 | 每轮都像新对话,任务稍长就断片 |
| 工具 | 让模型输出变成真实动作,而不是只回漂亮话 | 只会建议“你可以下单了”,不能真的下单 |
| MCP | 用统一协议接入各类服务,避免一个服务一套格式 | 接一个业务系统要写一大堆胶水代码 |
| 记忆 | 跨会话保持一致的人设、偏好和结论 | 同一个用户隔天再来,Agent完全想不起来 |
这四个模块在Hermes里有明确的调用顺序:用户消息进入后,先由会话模块组装上下文,再交给模型推理;模型如果请求工具调用,就进入工具模块;工具模块发现目标是MCP服务时,再转交给MCP客户端;模型需要参考历史信息时,记忆模块检索结果会追加到会话上下文里,但不会反向污染档案库。这种单向协作关系让我可以放心地单独替换任意一层而不影响其他层。
1.2 为什么选择这种设计
最笨的方案当然是把所有可能性都塞进提示词里,让人工智能自己“理解一切”。实测下来这种方案维护成本极高:每接一个新业务系统,都要改提示词、调样本,还经常出现提示词互相冲突。Hermes采用分层设计的核心逻辑是,让每一层能力只做一件确定的事,用程序保证结构的确定性,用模型负责语义理解的不确定性。
比如工具和MCP为什么要分开,而不是搅在一起?我踩过这个坑。如果所有MCP服务都直接挂在工具列表上,工具数量很快会膨胀到上百个,模型每次都要在上百个工具里做选择,准确率会明显下降,响应速度也变慢。Hermes的做法是把本地工具作为第一优先级的固定动作,把MCP作为二级外部能力池。会话模块只看到工具名,工具模块内部再去判断这个工具来自本地还是MCP,这样模型的选择空间可控,程序的路由清晰,日志也不乱。
2. 会话能力:Agent的底层引擎
2.1 上下文结构:Agent的“会话内存”里到底存了什么
在Hermes里,一条会话消息必须包含角色和内容两个基本字段,角色分别是系统、用户、助手、工具四种。模型看到的上下文就是一条按时间排好的消息列表,顺序不能乱。这里有一个新手经常忽略的点:工具调用的结果必须作为一条独立消息写回消息列表,而不是只存在程序变量里。否则,模型在下一轮生成时看不到刚才工具返回的数据,就只能靠猜。
messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": "帮我查一下今天项目目录下的文件变动"}, {"role": "assistant", "content": None, "tool_calls": [ {"id": "call_001", "name": "mcp__local-fs__list_dir", "arguments": {"path": "/data/projects"}} ]}, {"role": "tool", "tool_call_id": "call_001", "content": "今天有3个文件被修改,分别是..."}, ]整个会话循环的流程我是这样实现的:用户输入进入后组装messages,调用模型接口;如果模型返回里带tool_calls字段,就执行对应工具,把执行结果作为tool消息追加;然后再调用一次模型,让模型基于工具结果生成最终回复或继续提出新的工具调用。这个循环会一直重复,直到模型不再请求工具,或者达到我设置的最大工具轮数上限,默认是5轮。
这个循环是会话能力的骨架,也是最容易出问题的地方。很多人以为模型调用一次就完事了,实际上真实场景经常是连环调用:先查目录、再读文件、再根据文件内容调用另一个服务。每一层的结果都要正确写回消息列表,才能支撑这种连环调用。
2.2 上下文窗口:token预算怎么分、超限怎么办
上下文窗口是有限的,这是每个Agent开发者早晚都会撞上的墙。如果消息无限追加,模型会先忘掉最前面的系统指令,然后忘掉用户最初的目标,最后直接报长度错误。Hermes的做法是,给每个会话设置一个最大上下文token数,我当前在用的是32000。这里拆分一下预算:系统提示词大约占1200,用户的即时问题占500,剩余部分绝大部分留给历史对话和工具返回。
一旦消息总长度接近上限,就触发压缩策略:把最早的多轮消息打包,交给一个摘要模型压缩成一段约800字的阶段性总结,作为一条系统消息插回消息列表头部,原始消息则从列表中移除。这样做的代价是损失部分细节,好处是保住当前任务目标和最近几轮的关键反馈。压缩时机要在剩余token低于总量的15%时就执行,不要等到报错才动手。
def compress_if_needed(messages, budget=32000): current_cost = estimate_tokens(messages) if current_cost > budget * 0.85: early_messages = messages[:-6] # 保留最近6轮 summary = summarize(early_messages) messages = [ {"role": "system", "content": f"此前对话摘要:{summary}"} ] + messages[-6:] return messages还有一个细节:压缩不能在每轮都做。频繁压缩不仅消耗token,还会让模型时而看到完整历史、时而只看到摘要,造成行为不一致。我实测下来,当一个会话里消息数超过15轮时再压缩,效果比较稳定,更早触发反而容易把早期的关键约束丢掉。
2.3 会话配置的几个实用参数
Hermes在会话层有几个重要参数,直接影响任务的稳定性和返回风格。
温度参数要分场景设置。我当前的做法是:规划类任务用0.2,创意生成类用0.7,工具调用环节固定用0.1。工具调用对随机性极其敏感,温度稍微调高,模型就可能把参数名写错、把JSON格式写坏,所以我宁可在工具调用阶段牺牲一点“多样性”,也要保证格式稳定。如果你发现工具调用时灵时不灵,先去看温度是不是被设成了默认值。
单次输出长度也要限制,我通常设置max_tokens为2000。这看起来好像很保守,但实际效果很好:限制单次输出长度,可以避免模型一次性吐出超长文本把上下文窗口挤爆,同时也逼着模型把回复写得更精简。用户不会看又臭又长的回复,模型也不该一次性生成过多内容。
再强调一下系统提示词。这个字段在Hermes里的地位相当于“宪法”,所有的工具清单说明、边界限制、回复偏好都要写在这里。系统提示词不是写完就完事,我每次新增工具都会同步检查是否需要更新这里的说明,否则模型看不到新工具的存在,你后面加的一切能力都是白搭。
会话状态管理是整个会话层里最容易被低估的部分。Hermes会为每个会话维护一个状态对象,记录当前任务目标、已完成的工具步骤、剩下的待办步骤。一旦用户在半路切到别的话题,这个状态对象会把原有目标挂起,等用户切回来时再恢复执行。
{ "session_id": "sess_001", "active_goal": "整理上周五会议纪要并发送邮件", "pending_step": "调用email_send工具", "completed_steps": ["读取会议纪要文件", "生成邮件草稿", "确认收件人"], "suspended_at": "2025-01-17T10:23:00Z" }这个状态对象的引入,让Hermes从“一问一答”变成了真正的任务执行器。如果你只想做个简单聊天机器人,这个可以不要;但只要是做Agent,我的建议是越早引入状态管理越好,后面就不会返工。
3. 工具调用:打通Agent与外部世界的脚手架
3.1 工具调用的原理
模型本身并不执行任何动作,它的输出只是文本。工具调用机制的本质,是让模型输出一个结构化的意图,运行时拿到这个意图后去执行真正的函数,再把结果回写给模型。我在Hermes里管这个叫“意图-执行-回填”循环。你把它想成开会下指令:模型是发号施令的人,但它不亲自搬砖,工具模块是搬砖工,搬完后回来向模型汇报结果。
工具调用请求的格式一般是模型输出这样一段结构化内容:工具名、参数、调用标识。没有参数就传空对象,千万不要省略字段。Hermes在解析时会做严格校验,但凡字段类型不匹配或缺少必填字段,就直接报错,而不是默默吞掉。这看起来有点死板,但减少了后面大量不确定性问题。
3.2 定义工具时,参数描述越“笨”越好
这是我反复踩坑后才总结出来的规则。给模型用的工具描述,不需要文采飞扬,但要足够“笨”、足够直白。描述里应该写清楚这个工具是干什么的、边界在哪、什么情况下适合调用、参数格式要求是什么。
TOOLS = [ { "type": "function", "function": { "name": "query_order_status", "description": "查询订单当前状态,适用于售前、售中、售后全流程。一次只能查询一个订单。", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "pattern": "^[A-Z0-9]{8,12}$", "description": "订单号,由8到12位大写字母或数字组成" } }, "required": ["order_id"], "additionalProperties": False } } } ]参数约束越明确,模型越不容易瞎编。比如订单号我要么给regex表达式,要么给格式示例,否则模型可能传入小写、带横杠、甚至连空格都可能有。字符串枚举也要尽量穷举,实在无法穷举就写明格式。再强调一个细节:是否允许additionalProperties一定要设置,否则模型可能自作主张往里塞一堆你根本不需要的字段。
工具的执行结果也需要结构化。Hermes约定工具返回的是Json对象或按格式封装的字符串,同时会标注返回的数据结构说明。这份说明不是给用户看的,是给模型看的,让它知道结果里有哪些字段、各自含义是什么,这样才能准确转述给用户。返回结果直接抛一个长文本上来,模型反而容易抓不住重点。
3.3 实操:接入一个自定义查询工具
我用订单状态查询这个例子,完整走一遍Hermes的接入流程。场景是一个电商系统需要让Agent查用户订单状态。
第一步,写好工具函数本体,它负责真正调用订单服务的接口并返回结构化数据。第二步,把工具函数注册到Hermes的工具列表里,也就是上面那段JSON Schema。第三步,配置工具调度层的超时参数。第四步,发起测试会话。
from hermes.runtime import ToolRegistry def query_order_status(order_id: str) -> dict: resp = order_service_client.query(order_id=order_id) return { "order_id": resp.order_id, "status": resp.status, "last_update_time": resp.updated_at.isoformat(), "logistics_company": resp.carrier, "tracking_no": resp.tracking_number, } ToolRegistry.register( name="query_order_status", description="查询订单当前状态,适用于售前、售中、售后全流程", parameters=TOOLS[0]["function"]["parameters"], handler=query_order_status, timeout_seconds=10, )当用户问“我的订单到哪了”,模型看到工具列表里有query_order_status,就会输出一个工具调用请求,arguments里带上订单号。Hermes的工具调度层解析出order_id,调用order_service_client,拿到结果后封装成一条tool消息写回messages。模型再读一遍消息列表,基于这个真实结果组织最终回复:“您的订单已发货,承运方是某某物流,单号是...”
这里有一个非常关键的操作:工具结果必须回到会话上下文,而不是直接原样返回给用户。如果直接把接口原始JSON丢给用户,第一不友好,第二模型失去了参与感;只有把工具结果回填给模型,让模型基于结果做进一步判断,这个Agent才真的有Agent的样子。
3.4 工具调用容易踩的四个坑
超时问题是第一个坑。外部接口偶尔慢是很正常的,如果工具调用没有超时控制,整个会话循环会卡住,用户那边只能一直看到“正在处理”。Hermes默认给每个工具设置了15秒超时,超时直接返回错误,让模型决定是重试还是向用户说明。像文件上传、大批量数据处理这类长任务,要单独设置更长的超时时间,不能用默认值一刀切。
幂等是第二个坑。凡是会改变状态的工具,比如发邮件、改订单、创建工单,都要设计成幂等。不然模型在第一次调用超时后重试,就可能把同一封邮件发了两遍。我的习惯是这类工具必须接收业务侧唯一标识作为参数,比如客户端生成事件ID,这样服务端能对重复请求去重。
返回体过大是第三个坑。有的业务接口一返回就是几百KB,整个塞进上下文会直接占用对话预算。Hermes的做法是先用摘要服务压缩返回内容,或只提取模型需要的关键字段,再写回上下文。同时要保留一个“详情查询”开关,当用户需要看细节时再拉取完整数据。
权限校验是第四个坑,也可能是最严重的。模型生成的参数只是文本,它本身不能承担安全责任。工具函数内部必须做权限校验,确保当前身份有权访问目标资源。用户A的订单不能让用户B查,这不能靠模型“自觉”,必须靠工具函数内部硬校验。
4. MCP:统一外部依赖的标准协议
4.1 MCP是什么:Server、Client、Host三层
MCP全称是Model Context Protocol,它解决的是Agent接入外部服务时的“一次接入、处处复用”问题。没有这套标准之前,每个服务商各自定义一套API、认证方式、返回格式,Agent项目要接入一个系统就得写一大堆胶水代码。改了一下思路:把外部能力封装成标准化的MCP Server,Agent通过MCP Client去发现和调用这些能力,两者之间的连接和通信格式由协议统一规定。
MCP在概念上分三层:MCP Server是能力提供方,可以是本地子进程,也可以是一个远程服务,它维护一份工具清单,并负责真正执行工具;MCP Client是Agent这一侧的连接器,负责连接Server、获取工具清单、发起调用;Host是Agent运行时的宿主,也就是会话循环所在的进程。这样拆分的价值在于:更换一个外部服务时,我只需要改配置里的Server指向,不需要动Agent主程序。
Hermes在启动时会读取MCP配置,逐一连接所有声明的Server,把发现到的工具列表统合到工具调度层。这些MCP工具的命名空间和本地工具是分开的,通常会加上mcp__server__tool当前缀,避免重名冲突。这个设计让我一眼就能在日志里看出某个调用是本地工具还是MCP工具,排查问题方便很多。
4.2 MCP工具和本地工具到底差在哪
很多开发者刚接触MCP时,容易陷入两个极端:要么把所有能力都塞进MCP,要么干脆不用。我建议先分清楚这两者的定位。
| 维度 | 本地工具 | MCP工具 |
|---|---|---|
| 运行位置 | Agent子进程内 | 独立Server进程 |
| 接入方式 | 代码里直接注册函数 | 配置里声明Server地址 |
| 通信方式 | 进程内函数调用 | 标准协议报文 |
| 扩展性 | 需要改代码重新发布 | 加一份配置即可发现新工具 |
| 适用场景 | 确定性高、执行快、强依赖内部逻辑 | 跨系统、需要隔离、可能要频繁变更的服务 |
本地工具适合做那些时效性要求极高、又频繁调用的原子操作,比如字符串处理、内存查询、格式化。MCP适合做那些独立部署、权限边界清晰、可能来自第三方系统的能力,比如文件系统、数据库、邮件、搜索。我目前的使用经验是,把一个服务做成MCP Server,能省掉后续接入时的所有重复代码,但也不要因为图省事把Agent的内部函数强行拆成MCP,一来一回反而增加调试成本。
4.3 实操:让Agent通过MCP读本地目录
这一步我实测过很多次,配置方法相当简单。假设现在有一个文件系统类的MCP Server程序,我需要让Hermes能读取指定目录下的项目文件。只需在MCP配置文件里声明这个Server。
{ "mcpServers": { "local-fs": { "command": "mcp-file-server", "args": ["--root", "/data/projects"] } } }Hermes启动后,会通过MCP Client连接local-fs,自动发现read_file、list_dir、write_file之类的工具。这些工具会以mcp__local-fs__read_file这样的形式出现在Agent的工具列表里。用户问“看看项目目录里有哪些文件”,模型看到工具清单后返回工具调用,调度层识别出这是MCP工具,转交给MCP Client完成调用,结果回填会话,模型再生成自然语言回复。
调试MCP连接时我最常用三个手段。第一步,看Agent启动日志里有没有输出“MCP server connected”的状态。第二步,进入一个查看工具列表的调试接口,确认工具数量对不对。第三步,直接调用其中一个工具,检查返回格式是否符合预期。如果日志里能连上但工具列表是空的,九成是Server端没有正确声明工具清单,要去查Server自己启动时的报错。
4.4 MCP的安全边界
MCP让接入变容易了,也让风险面扩大了。最大的风险是:MCP Server的能力边界如果配得太宽,一个提示注入就可能造成严重后果。比如文件系统Server的root参数如果指向整个磁盘,模型一旦被恶意内容诱导,就可能把项目目录甚至系统文件读个遍。
Hermes这边的做法是最小权限原则。文件系统Server只授权一个子目录,数据库Server只授权一个业务库,远程MCP Server要加白名单地址,不允许随意连接公网。对Server返回的内容,我会做“数据化”处理:把它当作普通数据展示给模型,而不是直接拼进系统提示词,避免返回内容里隐藏的指令被模型当真。
还有一点必须强调:不要把敏感凭证写进MCP配置文件,比如数据库密码、API密钥。凭证应该走独立的密钥管理服务,MCP Server进程在独立环境里启动,只通过环境变量拿到它需要的那个最小凭证集合。配置都应该进版本管理,但密钥绝不能进。
5. 记忆能力:从一次性对话到持续协作
5.1 先分清两类记忆
记忆这个词在Agent领域已经被用烂了,但仔细拆开,其实是两类完全不同的东西。短期记忆就是上下文窗口内的消息列表,模型在每一轮都能看到它,对话结束就消失。长期记忆则要持久化存储,跨会话保留,Agent下次启动时还能读回来。没有长期记忆的Agent,每次见面都像第一天上班的新员工,用户已经说过一百遍的事情,它还是会问。
Hermes对短期记忆不做额外设计,完全靠会话模块的上下文管理兜底,因为短期记忆本质上是“当前任务正在处理的材料”。长期记忆则单独做了一套档案系统,存的是那些值得跨越多次会话保留的事实、偏好和结论。这两者不能混在一起管理,否则短期记忆里的临时信息会污染长期档案,长期档案里的大段背景又会在每一轮对话中浪费token。
5.2 记忆分层设计
Hermes的记忆模块分三层:会话上下文层、持久事实层、可检索档案层。会话上下文层已经在第二章讲过了,这里主要说后两层。
持久事实层存的是高度确定的信息,比如用户身份、偏好设置、显式交代过的规则。我通常用一个键值存储来管理,每条记录包含字段名、值、时间戳。这一层的特点是量小但价值高,几乎每条都要在每轮对话中作为背景信息带上。
可检索档案层存的是大段的对话摘要、业务对象记录、历史结论。这一层数据量大,不能每次都全量塞进上下文,只能根据当前问题进行检索,召回最相关的几条。检索结果的注入位置会在用户问题之后、模型推理之前,并且明确标注“以下为历史记忆,仅作参考”,让模型区分哪些是用户本次说的、哪些是历史记忆。
| 记忆层级 | 存储内容 | 读取时机 | 典型容量 |
|---|---|---|---|
| 会话上下文 | 当前任务的对话、工具结果 | 每轮 | 3万到10万token |
| 持久事实 | 用户偏好、身份、显式规则 | 每轮开头 | 不超过几百条 |
| 可检索档案 | 历史摘要、业务对象、结论 | 按需向量检索 | 可增长 |
5.3 实操:接一个向量检索的记忆模块
可检索档案层的实现,我的方案是“嵌入向量+向量检索+元数据过滤”。新记忆写入时,先把文本转成向量,连同元数据一起存到向量数据库;查询时,把当前问题转成向量,在库里做相似度搜索,再按元数据过滤掉不相关的领域。
class MemoryManager: def __init__(self, store, embedder, threshold=0.55): self.store = store self.embedder = embedder self.threshold = threshold def remember(self, text, metadata): vector = self.embedder.embed(text) self.store.add(vector=vector, text=text, metadata=metadata) def recall(self, query, top_k=5, scope=None): query_vector = self.embedder.embed(query) results = self.store.search(query_vector, top_k=top_k) if scope: results = [r for r in results if r.metadata.get("scope") == scope] return [r for r in results if r.score >= self.threshold]这里有个关键配置:相似度阈值。如果没有阈值,不管查询和记忆多么不相关,检索都会返回top_k条,模型就会被一堆无关记忆干扰。我在Hermes里设的默认阈值是0.55,低于这个分数的记忆不会进入上下文。具体数值要根据嵌入模型的实际表现去调,我建议上线前用一批带标签的查询做一次校准。
元数据过滤也非常有用。比如同一套记忆库里可能存着用户A和用户B的档案,查询只能基于当前会话的用户ID做过滤,不然后果不堪设想。我习惯在写入每条记忆时都带上用户ID、时间范围、业务域这几个维度,召回时逐层过滤。
记忆的写入时机要克制。不是所有对话都值得记住,临时寒暄、一次性任务、无意义的中间状态都不该写入。真正值得写的是三类信息:用户明确说出的偏好、跨会话有复用价值的结论、业务侧的关键对象状态。比如用户说“以后邮件附件用PDF格式”,这是偏好,必须写;用户说“今天天气不错”,这是废话,不用写。
5.4 记忆的冲突与遗忘
记忆系统运行一段时间后,冲突和过时问题会逐渐浮现。用户上个星期说“我用Outlook邮箱”,这周说“我改用Gmail了”。如果两段记忆都留在库里,模型在检索时可能同时拿到两条,然后一脸困惑。Hermes的处理原则是:对同一业务域的冲突记忆,以时间戳较新的为准;对于无法确定是否冲突的,保留两条并标记时间,让模型在回答时都参考。
遗忘机制也应该有。用户明确要求“删掉我的交易记录”,这不只影响一次会话,必须永久删除相关记忆,没有商量余地。另外,我给每条记忆都加了过期时间。临时性的偏好可以设30天,业务结论可以设90天,到期后自动进入待清理队列,由后台任务删除。记忆系统最怕“只进不出”,无限囤积只会让召回精度越来越差。
还有一个小技巧:不要把所有记忆都一股脑交给模型。一次召回3到5条足够,超过这个量,模型非但用不上,还容易受噪声干扰。宁可少记忆,也不要让陈旧信息在关键任务上误导模型。
6. 常见问题与排查记录
6.1 对话越长,模型越“健忘”
症状:对话进行到十几轮之后,模型忘了用户最初的任务目标,回答开始跑偏。排查时我先看上下文压缩是否生效,再看系统提示词里是否还保留当前任务描述。很多时候问题出在压缩把早期目标摘要丢了,模型后面只看到零散反馈,当然容易跑偏。解决手段是:压缩时强制保留“用户最初目标”字段,把它放到系统提示词的固定位置,不随压缩丢弃。再就是把触发压缩的阈值调高一点,别让摘要压缩得太频繁。
6.2 工具调用时灵时不灵
症状:同样一个任务,有时候能正确调用工具,有时候直接编一个结果出来。这个问题的元凶大多是温度设置或工具描述模糊。温度太高会让模型在“调用工具”和“直接回答”之间摇摆,我建议把工具调用阶段的温度固定到0.1左右,不要让它自己去模糊决策。另一个元凶是工具描述太抽象,模型不知道什么时候该用这个工具。比如为了合规问题,给查询订单工具加上场景说明,模型通常就不敢瞎猜了。
6.3 MCP Server连不上或工具列表是空的
症状:Agent启动一段时间后,所有mcp__前缀工具都不见了,调用时报连接失败。排查顺序很固定:先看MCP配置文件路径对不对,再看Server命令是否存在、有没有执行权限,接着手动启动Server看有没有报错,最后检查Agent的启动环境变量是否包含了Server需要的认证信息。我遇到最多的情况不是协议问题,而是配置路径写错或Server进程没有启动权限。确认本地能手动跑通后,再去Agent这边连接,基本就顺了。
6.4 记忆召回了不相关内容
症状:用户问今天的会议安排,结果系统塞进来一堆上个月的闲聊记录,模型回答明显被带偏。优先检查top_k是不是设得太大,默认5条已经足够。再检查相似度阈值有没有生效,很多封装好的向量库默认返回的分数不是标准化相似度,需要先确认它们的分数区间。最后检查元数据过滤条件是否真的传到了检索语句里,漏了scope过滤是家常便饭。
6.5 快速排查速查表
| 现象 | 优先检查 | 大概率原因 |
|---|---|---|
| 模型忘记早期任务 | 系统提示词中的目标字段 | 上下文压缩把目标摘要丢了 |
| 工具返回结果未被采用 | 返回结果是否作为tool消息回填 | 回填时机错位,模型看不到结果 |
| MCP工具列表为空 | Server启动日志 | 配置路径或进程权限问题 |
| 召回记忆与问题无关 | 相似度阈值和top_k | 阈值过低或过滤条件缺失 |
| 模型频繁编造工具参数 | 工具参数Schema | 缺少regex、枚举或示例 |
这篇内容验证下来,我最大的体会是Agent不是越复杂越好,而是每一层能力都要有确定的边界。会话管好消息流,工具管好真正执行,MCP管好外部接入,记忆管好跨会话的知识,四者有清晰的划分,系统才谈得上稳定。如果你正在搭建自己的Agent,我建议先把会话和工具跑通,再加入MCP,记忆最后做,因为记忆系统一旦设计错了会反噬前面的所有能力。