2026/10/13 17:26:57

多模态、Harness与MCP:构建稳定Agent系统的三层架构

多模态、Harness与MCP:构建稳定Agent系统的三层架构 大模型多模态、Harness 与 MCP一个 Agent 开发者的技术笔记最近在做一个多模态 Agent 项目过程中把“多模态输入处理”“Harness 设计”“MCP 协议接入”这三块内容完整地串了一遍。说实话这三块单独拿出来都有大量资料但真正把它们放进同一个 Agent 系统里协同工作时才会发现坑远比想象的多。这篇笔记不打算泛泛讲概念而是想围绕我实际开发中的技术决策、踩坑经历和最终沉淀下来的方案聊聊一个 Agent 开发者眼中这三者到底是什么关系以及怎么把它们组装成一个能真正跑起来的系统。如果你正准备做多模态 Agent、或者正在为工具调用集成发愁这篇内容应该能帮你省下不少试错时间。先说结论多模态决定了 Agent 能“感知”什么Harness 决定了 Agent 怎么“思考和行动”MCP 则决定了 Agent 能“触达”哪些外部能力。三者不是三个独立模块而是同一套运行时里互相咬合的三层结构。下面按我的理解逐层拆开讲。1. 多模态Agent 的感知层比你想的更复杂1.1 多模态不是“能看图”这么简单很多人以为多模态 Agent 就是把图片丢给视觉大模型让它描述一下内容然后继续对话。实际上当多模态输入进入一个 Agent 系统时它会被用在完全不同的几个环节用户意图理解、环境状态感知、工具参数提取、结果验证反馈。每个环节对图像/音频/视频的处理要求都不一样不能一概而论。以我做的一个“工单自动分类 现场图片辅助判断”的场景为例。用户上传一张设备损坏照片Agent 需要做三件事第一判断照片里是否存在可见损伤第二结合文字描述确定工单优先级第三决定是否需要调起维修知识库查询。如果只是把图片和文字一股脑塞给模型虽然大模型能回答但回答质量和稳定性都不可控。这里有个核心问题多模态输入并不是“免费”的。一张高分辨率图片转成视觉 token 之后动辄上千个 token如果 Agent 在每一轮思考中都反复携带这张图的完整表示上下文很快就会爆掉而且推理时延会明显上升。我实测过同样一张 1024x1024 的图片在典型视觉语言模型里约消耗 1000 到 2000 个视觉 token这还没算对话历史和工具返回结果。如果 Agent 需要多轮调用工具每一轮都把图片重新编码一次成本几乎是成倍增长。所以在真实项目里我的做法是把多模态内容“前置消化”而不是让模型每一轮都直接看图。具体来说用户上传图片后先触发一个“感知摘要”环节用视觉模型一次性提取关键信息如损伤类型、位置、严重程度把结果压缩成结构化字段和一小段文字摘要再放进 Agent 的主上下文。后续推理循环只携带这份摘要原图只在必要时重新引用。1.2 视觉输入的工程化预处理我总结了一套比较稳妥的多模态输入预处理流程分四步走格式归一不同来源的图片格式、尺寸、方向各不一样。我会用统一工具把图片转为标准格式如 JPEG并把长边缩放到 1024 或 1536 像素以内。这一步能显著降低 token 消耗同时避免某些模型对超大尺寸图片的处理异常。质量预检模糊检测、过暗/过亮检测、是否包含文字。很多场景下用户上传的照片质量很差直接丢给模型会得到“看不清”这类无效回答。预检可以在工具层就拦截低质量图片引导用户重新拍摄。OCR 先行如果图片中包含文字信息如仪表读数、屏幕截图、单据先用 OCR 工具提取文本。OCR 结果作为结构化文本进入上下文比直接把图丢给模型更可靠尤其是中文场景下端到端视觉模型的文字识别能力虽然进步很大但复杂背景下仍会出现漏字、错字。视觉摘要提取用一个固定 prompt 让视觉模型输出 JSON 格式的结构化描述包括场景类别、关键物体、异常状态、疑似问题等。这个摘要成为 Agent 对图片的“第一印象”。经过这四步处理后我实际放进主上下文的不是原始图片而是原始图片的缩略路径必要时才加载 OCR 文本 视觉摘要 JSON。这样处理后一张图片的有效上下文开销从一两千 token 降到了三四百 token而且信息密度更高。1.3 音频和视频更重的感知负担音频和视频的情况比静态图片更复杂。音频通常需要先转写ASR成文本再进行语义分析。这里有个细节不要只转写“说了什么”还要提取“怎么说的”。说话人的语气、语速、停顿在工单质检、客服场景里往往比文字内容更重要。如果只做 ASR 文本转写这些信息会全部丢失。我试过的方案是转写文本 音频级特征摘要音量起伏、静音段分布、说话人数量一起送入 Agent。视频则更麻烦一般做法是抽帧 分镜摘要而不是把整个视频喂给模型。抽帧间隔取决于场景平稳画面可以每 5 到 10 秒抽一帧动作变化剧烈的场景可能需要每秒抽帧。抽出来的帧再按图片流程处理。做多模态感知设计时我想提醒一个容易被忽略的点感知结果本身也可能出错。OCR 会漏字视觉摘要会误判ASR 会听错。因此 Agent 的 Harness 里必须给感知结果留出“置信度”和“回退策略”。比如 OCR 置信度低时不能硬用这份结果去触发后续动作而是先让 Agent 基于原始图片再做一次确认性调用。2. HarnessAgent 的运行骨架也是稳定性分水岭2.1 为什么不能只写一个“循环调模型”的脚本做 Agent 开发的人几乎都经历过这个阶段写一个 while 循环调模型如果返回里有 tool_calls 就执行工具把结果拼回去再调模型直到模型不再调用工具为止。这个玩具代码在 demo 里跑得很欢一旦上生产就各种翻车。问题在于真实场景里的 Agent 循环远不止“调模型”这么简单。模型可能超时、可能返回无法解析的工具参数、可能在同一个问题上反复调用同一工具死循环、可能因为上下文过长而崩溃。如果没有一层稳固的运行时把这些状况都接住Agent 根本没法稳定工作。我理解的Harness就是这个负责“接住一切”的运行时骨架。它定义了 Agent 如何思考、如何调用工具、如何管理记忆、如何恢复错误、如何控制上下文用量。它不包含具体的业务逻辑但所有业务逻辑都在它之上运行。Harness 的设计质量直接决定了一个 Agent 系统是“看起来智能”还是“真的稳定”。2.2 Harness 的核心模块我自己的 Harness 设计经历了三个版本迭代最终沉淀出下面这几个核心模块缺一个都会出问题状态管理State ManagerAgent 每一轮的状态至少包括当前目标、对话历史、工具结果缓存、剩余步骤预算、临时变量。我把这些统一放进一个可序列化的状态对象里每一轮循环结束都持久化一次。这样不管中间哪一步崩了都能从最近的状态恢复而不是从头再来。步骤控制Step Controller不能让模型无限思考下去。我严格控制最大循环轮数通常 5 到 10 轮每一轮开始前检查剩余预算预算耗尽时强制 Agent 进入总结阶段把已获得的信息整理成最终回复。这样既保证了任务完成度又避免了死循环拖垮整个系统。上下文管理Context Manager这是 Harness 里最容易出彩也最容易被忽视的模块。核心目标是在有限的上下文窗口内尽量保留“对当前任务最重要”的信息。我的策略是分层存储系统提示词和工具定义固定在底部区域任务目标、最新状态放在最前历史消息按关键程度压缩旧消息折叠成摘要工具返回内容只在被后续步骤引用时保留完整版本否则只保留摘要。工具调度Tool Dispatcher模型返回的工具调用意图是结构化的但执行环节还需要处理参数校验、超时控制、并发限制、幂等等问题。工具调度器负责把模型输出的“我想调用这个工具”变成一次真实、可控的外部调用。这里给一个极其简化的 Python 伪代码展示 Harness 主干循环的样子class SimpleHarness: def __init__(self, model, tools, max_steps8): self.model model self.tools {t.name: t for t in tools} self.max_steps max_steps def run(self, user_input): state State(user_inputuser_input) for step in range(self.max_steps): messages self.context_manager.build(state) response self.model.chat(messages, toolsself.tool_specs()) if response.has_tool_calls(): for call in response.tool_calls: try: result self.tools[call.name].execute(**call.arguments) except Exception as e: result ToolError(str(e)) state.add_observation(call.id, result) state.mark_step_done() continue # 没有工具调用认为任务收敛 return response.content return self.finalize(state)这段代码去掉了很多细节并发、重试、记忆压缩、模型超时等但主干结构是真实的。我建议任何开始做 Agent 的人都先从这样一个最小 Harness 写起跑通之后再逐步往上加复杂度。2.3 Harness 里的几个常见设计模式在 Harness 层面现在已经沉淀出几种被广泛使用的思考编排模式我按自己的理解做个对比ReAct 模式模型交替输出“思考 → 行动 → 观察”。实现最简单适合工具调用链路较短、目标明确的任务。缺点是长任务时思考文本会产生大量中间 token 消耗且容易在复杂任务里迷失主线。Plan-and-Execute 模式先让模型输出一个整体计划Plan然后逐条执行计划中的步骤。好处是任务结构清晰适合多步骤流水线作业缺点是计划可能一开始就定错执行到一半发现计划不现实时需要额外的“重新规划”能力这在 Harness 里要单独实现。多 Agent 编排拆成多个子 Agent如研究者、执行者、审查者每个 Agent 有自己独立的 Harness 循环。适合任务复杂度高、需要角色分工的场景。代价是系统复杂度和成本明显上升子 Agent 之间的通信协议需要仔细设计否则容易变成“开会扯皮”。我的建议是先做单 Agent ReAct 模式跑通业务闭环再根据瓶颈决定是否引入 Plan-and-Execute 或多 Agent 架构。不要一开始就上复杂编排否则你很难分辨到底是模型能力不够还是编排逻辑有 bug。2.4 Harness 与模型无关性一个容易被忽略的设计原则Harness 应该尽量与具体模型解耦。因为模型本身在快速迭代今天用的模型和三个月后的最优模型可能完全是两回事。如果 Harness 里写死了某个模型的输出格式、某个模型的特殊指令风格换模型时就会异常痛苦。我的做法是把所有模型调用统一封装为一个接口输入是消息列表 工具定义输出统一成标准结构文本内容、工具调用列表、token 统计、结束原因。底层接哪家模型、用什么参数都通过配置文件切换。这样换模型时只需要改适配层Harness 完全不用动。3. MCP工具集成的标准化协议3.1 为什么集成工具这么痛苦在做 Agent 之前我做过很长时间的传统服务端开发。传统服务端集成外部系统一般就是调 API、写 SDK、处理鉴权。但 Agent 的工具集成多了一个非常特殊的麻烦Agent 的工具调用不是预先编排好的固定流程而是模型根据上下文动态决定的。这意味着你不仅要把工具“接通”还要让模型理解这个工具是干什么的、什么条件下用、参数该怎么填。在 MCP 出现之前每个人都在自己写一套工具封装有的用 JSON Schema 描述工具参数有的直接写自然语言说明有的把多个工具塞在一个接口里靠参数区分。结果就是 A 项目沉淀的工具很难迁移到 B 项目社区里几乎没有可复用的工具生态。MCPModel Context Protocol模型上下文协议解决的就是这个问题它定义了一套宿主Host与工具服务Server之间的标准通信方式工具的描述、参数定义、调用方式都遵循统一规范。只要 Agent 端的 Harness 实现了 MCP 客户端任何遵循 MCP 标准的工具服务器都可以直接接入不需要再写定制胶水代码。3.2 MCP 的核心抽象把 MCP 拆开看核心抽象其实不多但每个都很关键Host运行 Agent 的宿主程序也就是你的 Harness 或应用本身。Host 负责理解模型意图、调用工具、向用户展示结果。Client 与 ServerHost 内嵌 MCP Client与远端或本地的 MCP Server 建立连接。一个 Server 可以暴露多个 Tool、Resource 和 Prompt。数量不对等一个 Host 可以同时连接多个 Server每个 Server 也可以被多个 Host 连接。Tool可以被模型调用的函数。定义包括名称、描述、参数 JSON Schema。这是最常用的原语基本等于“给模型一个可动手的按钮”。Resource可读取的数据对象比如静态文档、数据库查询结果、照片。注意 Resource 和 Tool 的区别Tool 是会改变状态或执行计算的动作Resource 是数据本身。Prompt可复用的提示词模板用于规范某类任务的输入。这不是必需品但对于标准化的业务场景很有用。MCP 的典型交互流程是这样的Host 先与 Server 完成握手获取 Server 暴露的所有 Tool 列表然后把 Tool 列表含 JSON Schema交给模型作为“可用能力”模型在推理时自主决定调用哪些 Tool 以及传入什么参数Server 执行 Tool 后返回结果Harness 把结果作为“观察”交给模型继续推理。3.3 实现一个最小 MCP Server在 Python 环境中官方 SDK 已经提供了一套非常顺手的工具类。我实现过一个“业务查询 本地文件读取”的最小 MCP Server核心代码大概长这样from mcp import Server, types from mcp.server import stdio_server app Server(order-assistant) app.tool() async def query_order(order_id: str) - str: 根据订单号查询订单状态。 # 这里替换为真实的业务系统调用 return f订单 {order_id} 状态: 已发货预计 3 天后送达 app.tool() async def read_local_file(path: str) - str: 读取本地文件内容注意路径必须在允许目录内。 return await safe_read(path) async def main(): async with stdio_server() as (read, write): await app.run(read, write)这里用了 stdio 作为传输层是最简单的接入方式。生产环境通常建议走 Streamable HTTP 或 SSE以便支持远程部署但本地工具和隐私敏感工具用 stdio 反而更安全、更简单因为进程边界本身就是隔离。3.4 MCP 与 Harness 的集成协议到指令的翻译MCP 解决了“工具怎么连”但真正接入 Harness 时还有一个关键环节把 MCP 的工具元数据翻译成模型能理解的格式。不同模型的工具调用格式有一些差异但绝大多数都兼容 OpenAI 风格的 function calling。所以我做了一个适配层把 MCP 的 Tool 定义名称、描述、JSON Schema映射为模型接口要求的工具格式。映射时有一个细节我踩过坑MCP 的描述文本通常偏技术化适合开发者阅读但模型理解力是有限的描述里如果堆砌太多实现细节模型反而不清楚什么时候该用这个工具。我通常会在描述末尾追加一句“什么时候用/什么时候别用”的自然语言说明。举个例子原始描述: 查询订单状态接口接受订单号参数返回最新物流轨迹及状态码 优化描述: 当用户问“我的订单到哪了”或“发货没有”时使用此工具。传入完整订单号。查询不到时返回空结果不要编造信息。后者的模型调用准确率在我实测中明显高于前者。这里其实体现了一个规律工具描述不是为了“说明功能”而是为了让模型在正确的决策点选中正确的工具。MCP 还有一个我认为非常有价值的能力动态发现——Host 可以在启动时甚至运行中连接 Server 并动态获取工具列表这意味着你可以把一组工具做成一个“插件包”不同项目按需加载。对比以往把工具写死在 Harness 里这种动态插拔机制对项目复用非常有帮助。4. 三者的协作一个能落地的 Agent 系统4.1 从感知到行动的完整链路有了上面的基础我把多模态、Harness、MCP 三者放在一起看一个生产级 Agent 系统的完整链路是这样的用户输入可能是文本 图片/音频/文件经过格式归一和前置感知处理转成“文本 结构化摘要”的统一表示。Harness 启动循环后先让模型基于用户意图和多模态摘要进行规划如果任务需要外部数据或工具能力模型从 MCP 服务器发现的工具列表中做出选择调用工具后Harness 校验返回结果、更新状态继续下一轮推理直到模型认为任务收敛或步骤预算耗尽最终为用户生成答案。这个链路里多模态感知层负责“看懂世界”MCP 层负责“够到世界”Harness 层负责“在思考与行动之间稳定地来回切换”。三者缺一不可没有多模态感知Agent 面对图片只能瞎猜没有 MCP每个工具都要写一套专用对接代码没有 Harness前面的感知和工具调用就是无序的一次性动作根本跑不出稳定结果。4.2 一个具体的执行流程示例我用一个“售后客服 Agent”场景来完整走一遍流程方便理解用户上传一张破损商品照片并留言“收到的水杯碎了怎么办”感知层OCR 识别照片中是否包含订单号或快递单号视觉摘要提取出“杯子”“碎裂”“破碎位置明显”等结构化信息。Harness 第一轮模型看到用户意图是“售后理赔”感知摘要显示破损属实。规划逻辑触发“查询订单 获取售后政策”两个需求。工具调度模型调用 MCP 工具query_order(订单号)和search_policy(商品类型水杯, 问题类型破损)两个工具可能分属不同的 MCP Server。工具返回后Harness 把结果拼接进入上下文模型判断信息充足生成最终回复“根据照片和订单信息您的水杯在运输中破损符合退款条件售后链接已生成点击即可办理。”整个流程如果出现异常比如订单号识别失败Harness 会捕获错误模型会收到错误提示然后自动改为追问用户补充订单号而不是崩溃或编造。这是一条非常常见的真实链路每一步都没有“魔法”全靠前两层的机制设计兜底。4.3 参数选择的经验参考多模态 Agent 的参数配置没有标准答案但整体逻辑是有规律可循的。我根据实测经验整理了几个关键参数的选择思路供参考参数项我的推荐选择逻辑多模态图片压缩长边 1024 像素内token 与细节的平衡点超大图只会增加成本性能提升不明显感知摘要 JSON 长度300 到 500 token太短丢信息太长占上下文结构化的关键就是克制最大循环轮数5 到 10 轮低于 5 轮复杂任务容易没做完就被打断高于 10 轮死循环风险激增模型 temperature0 到 0.3Agent 工具调用追求确定性温度高了工具参数容易飘工具调用超时5 到 15 秒短了误伤慢接口长了用户等待不可接受最好按工具类型分级设置上下文保留策略旧消息摘要化工具结果按需加载保留必要信息控制成本工具返回大文本尤其要截断4.4 部署形态的选择根据业务需求我通常把部署形态分成三种各有优劣单机本地部署MCP Server 和 Harness 都在本机通过 stdio 或 local socket 通信。优点是延迟低、隐私安全、调试简单适合个人工具集成或内部数据敏感场景。缺点是所有能力集中在单机扩展性受限。混合部署Harness 云端运行MCP Server 分成本地与远程两组。本地 Server 连接私有数据远程 Server 连接公共能力。优点是兼顾隐私与扩展缺点是网络连接本身可能出问题需要在工具调度层做好连接管理和重试。纯远程部署所有服务都在云端MCP 通过 HTTP/SSE 传输。优点是统一运维、易扩展缺点是延迟受网络影响大、鉴权要求高。适合面向 C 端的线上服务。更多时候我是按安全级别来部署的凡是涉及用户隐私数据的 MCP Server一律靠近用户侧凡是通用能力如搜索、天气、计算放云端即可。5. 常见问题与排查实录5.1 多模态输入的常见坑坑一图片 token 占用轻松击穿上下文。我遇到过一次用户上传了一张 4K 分辨率的产品图光图片就吃了近 3000 token加上对话历史和工具返回直接触发了上下文超限。现在我的方案是上传后立刻压缩并在系统提示词里强制说明“只使用摘要信息不引用原始图像”。坑二OCR 结果不可靠却直接触发了业务动作。比如 OCR 把订单号里的 0 识别成 O系统拿着错误订单号去查询返回空结果。模型如果不理解“查询结果为空”与“订单不存在”的区别就会给出错误结论。我的解决方式是在工具返回空结果时附加一条说明“请检查参数是否有误或请用户重新提供”引导模型进行澄清而不是下结论。坑三视觉摘要存在“粉饰”倾向。有些视觉模型在描述图片时会倾向于说“一切看起来正常”即使图片有明显问题。这可能是训练偏好导致的。为了对抗这种倾向我会在视觉摘要的 prompt 里强调“关注异常和可疑之处”并要求逐项检查如“是否有破洞/污损/变色”而不是开放式描述。5.2 Harness 运行的典型问题死循环模型反复调用同一个工具且参数不变。排查时你会发现驱动它的“理由”其实站不住。我的解决方案有三层步骤预算硬限制工具调用去重——如果同一个参数在连续两轮中都被调用Harness 会拦截并提醒模型“该尝试已尝试过”设置同一工具最大调用次数通常是 3 到 5 次。模型幻觉工具返回值偶尔模型没有真正执行工具却在回复里“假装”拿到了结果。这类问题在弱模型上更常见。排查方法是让 Harness 在模型输出中严格检查凡是在回复正文里出现“查询结果为”“根据订单信息”等断言必须对应到已执行工具返回的真实内容。我在 Harness 里加了一个简单的溯源检查模型的最终回复里不得包含工具返回中不存在的事实。5.3 MCP 连接与调用的排查如果遇到 MCP 工具不生效我建议按照下面的顺序排查现象可能原因排查步骤Host 发现不了工具服务端未正常启动 / 协议不匹配先用 MCP 官方调试工具连接 Server检查工具列表是否正常返回模型从不调用某工具工具描述不清晰 / 工具不在当前模型上下文检查工具描述是否包含触发条件尝试把描述改得更具体工具调用时报参数错误JSON Schema 与模型生成的参数不匹配在工具层打印实际收到的参数与 Schema 逐字段核对工具执行超时网络波动 / 服务端处理慢区分长任务工具单独设置更长超时必要时改为异步回调模式工具返回内容过大Server 返回了完整数据在 Server 侧限制返回长度只返回模型真正需要的字段另外还有一个容易被忽视的坑如果你在本地调试 stdio 模式Host 进程意外退出后MCP Server 子进程可能残留。长时间开发会积累一堆僵尸进程。我处理的办法是统一用一个进程管理脚本确保 Host 退出时自动终止所有子进程避免端口和句柄泄漏。6. 最后再分享一些实操心得做 Agent 开发一年多来我最大的感受是这个领域没有“银弹”每层都有它各自的复杂性在等你。多模态层给我的教训是感知能力再强也要设计好信息压缩和错误回退机制否则你的上下文窗口会先崩溃。Harness 层给我的教训是宁可多写一点状态管理和容错代码也不要迷信“模型足够聪明能自我纠错”。模型在单轮对话里很聪明但在几十轮的工具调用循环里稳定性完全取决于 Harness 兜底得够不够全面。MCP 层给我的感触则是协议标准化带来的复用价值远超预期一旦你习惯了“工具即插即用”的开发方式就再也回不去写死工具列表的老路了。如果让我给刚开始做 Agent 的开发者一个最小起步建议我会说先不要追求大而全。选一个单一业务场景比如“客服工单分类”用最简单的 ReAct Harness 一个 MCP Server暴露两三个工具跑通闭环然后逐步加入多模态感知、上下文管理、错误恢复这些增强能力。你会发现每一个环节在你遇到真实问题后都会获得比任何教程都深刻的理解。这篇笔记里没有太多“业界前沿”的东西都是已经被我验证过的基础方案但基础方案的好处就是可复现、可依赖。希望这份笔记能给正在搭建 Agent 系统的你一些帮助。如果你在自己的项目里遇到了我上面提到的坑欢迎留言聊聊各自的解法。后续我还会抽时间把上下文管理和工具调度这两块展开细写那里面的细节决策也不少。