多模态、Harness与MCP:搭建稳定Agent工作流的关键实践
上午我丢给 Agent 一张库存截图让它“看看哪些 SKU 快断货了顺便把补货申请单建好”。它“看懂”了截图里三列数据却在调用补货工具时把“仓库 ID”填成了“商品条码”。我盯着报错日志愣了好几秒。这种问题几乎每个做 Agent 开发的人都遇见过。当模型不再只是回答你的问题而是需要自己去观察、检索、调用工具、完成任务时你很快会发现真正难的不是让模型“理解”而是搭一套能让它稳定干活的基础设施。这套东西业内通常叫Harness也就是 Agent 运行时/编排框架。而“看”这件事涉及大模型多模态能力“调用工具”这件事现在越来越多的开发者用MCPModel Context Protocol模型上下文协议做标准化。多模态、Harness、MCP这三个关键词正好戳中了我这段时间技术笔记的核心。这篇文章是我的个人实践记录内容偏工程向先讲 Harness 为什么存在再说多模态输入如何在 Agent 内部流转之后聊 MCP 能解决什么、不能解决什么最后给出一个“看图→查库存→建补货单”的完整链路实测以及我踩过的一些坑。如果你也在搭 Agent 框架或者正被“模型会聊天但不会干活”折磨这篇应该能帮到你。1. 为什么 Agent 需要 Harness从“问答模型”到“干活的人”的架构演进1.1 从“prompt 进、文本出”到“自主工作循环”中间缺的就是 Harness最早我们调大模型说白了就是“prompt 进文本出”写一段提示词模型给你一段回答。这个模式用在聊天、翻译、文本归纳上没问题但一旦任务变成“看一张截图查询库存再创建一张补货单”事情就变了。这不是一次性生成而是一个多步骤的工作流先理解截图然后决定要查什么调用查询工具拿到实时数据对比截图和数据判断哪些需要补货最后调用建单工具。每一步的结果都可能影响下一步的输入模型自己没法独立完成这套循环必须在外面有一个程序来负责当前执行到哪一步了、上一步返回了什么、下一步该选哪个工具、工具返回的数据是否正确、整个过程有没有超出安全边界。这个“外面的程序”就是 Harness。我最早踩过的坑是把这个循环写死在业务代码里用 if-else 把每个步骤串起来模型只负责其中一小段。结果只要任务稍微变一下代码就要改一遍。后来我意识到需要把“任务的执行逻辑”从“具体业务规则”中抽离出来让模型负责推理和决策Harness 负责承载和约束。这就像把一个只会给固定配方炒菜的师傅升级成一个能根据现有食材、厨具、食客口味临场发挥的主厨而 Harness 就是后厨备菜台、炉灶、调味料架、出锅检查全都摆好了主厨只需要决定怎么做。1.2 一个可用 Harness 的最小骨架五个模块缺一不可我按“最小可用”原则拆过一个 Harness长期稳定运行的版本里下面五个模块是必须的状态容器保存当前任务上下文包括所有历史消息、中间结果、已调用工具返回的数据、标志位。多模态场景下还要保存图像引用或压缩后的图像描述。工具注册表所有 Agent 可用工具的描述、入参 Schema、调用入口。Harness 把这个注册表动态注入给模型模型看到“有哪些工具、各干什么用”才能做选择。推理循环这是核心调度器。循环执行“组装上下文 → 调模型 → 解析模型输出 → 如果是工具调用则执行并回填结果 → 再组装上下文 → 继续调模型”直到模型输出最终回答或达到最大步数。消息历史/记忆管理负责裁剪、压缩、摘要历史消息控制上下文长度。这个在纯文本场景就很重要多模态之后更是生死线。审计与日志记录每次模型调用、每个工具调用的入参、出参、耗时、token 消耗。没有这个出了错只能对着黑盒猜。这里给出一个我常用的最小推理循环伪代码方便对照理解async def run_agent(task, tools, max_steps10): state AgentState(tasktask, history[]) for step in range(max_steps): context build_context(state) response await llm.chat(context, toolstools) if response.has_tool_calls(): for call in response.tool_calls: result await execute_tool(call) state.history.append(result) state.steps_taken 1 continue if response.is_finish(): return response.content return reach_max_steps_timeoutbuild_context负责把任务描述、历史消息、上一次工具结果拼成模型输入execute_tool则通过工具注册表找到对应实现。整个循环的关键在于模型永远看到的是“加工过的上下文”而不是原始日志这样它能专注于下一步决策。1.3 为什么我不把流程逻辑直接写进 Prompt有人会说既然模型这么聪明把工具说明写在 Prompt 里让它自己调不就行了吗我一开始也这么干过后来放弃了原因很现实可控性。PrompT 里的规则是“软约束”模型可能在某些措辞下绕过它。把关键逻辑放在 Harness 代码里是“硬约束”比如工具白名单、参数校验、敏感操作二次确认这些必须由代码强制保证。成本。所有工具说明如果全塞进系统提示每次调用都在烧 token。Harness 可以按任务动态加载相关工具而不是一股脑全给模型。可观测性。业务规则写死在 Prompt 里出问题你没法快速定位是模型没遵守还是工具本身返回了异常。Harness 结构化成日志后排查路径清晰得多。安全边界。比如模型无论如何都不能调用“删除数据库”工具这种限制必须写在 Harness 里而不是期望模型自觉。提示把模型当“决策大脑”不要把它当“流程引擎”。流程控制、权限校验、状态流转这些事情交给 Harness 代码来做模型只负责中间的判断和选择。2. 多模态并不只是“能看图”输入范式与内部数据流的重构2.1 一张图在视觉语言模型内部到底经历了什么多模态大模型VLM与传统纯文本模型最大的区别是输入侧多了一个视觉编码器。图片进入模型后会被切分成固定大小的图像块patch每个 patch 经过视觉编码器变成一个向量再通过投影层映射到文本 token 的语义空间与文本 token 拼成一个长序列交给后面的 Transformer 做统一推理。换句话说模型并不是“看”到一张完整图片而是把图片变成了“一长串视觉 token”再和问题文本一起滚动计算。这也是为什么很多 VLM 在视觉推理任务上表现不错——因为它本质上是把“看图”转换成了“读一串带空间信息的 token 序列”文本推理能力越强视觉问答通常越稳。但这也带来一个残酷的数学问题一张高分辨率截图产生的 token 数量非常惊人。我用某个开源 VLM 做过实测一张 1080P 的电商后台库存截图在默认配置下大约会被折算成 3000 到 5000 个视觉 token是同等长度文本问题的好几倍。这意味着如果 Harness 直接把截图原图丢给模型光是“看”这一步就可能在 token 开销上吃掉大量预算。2.2 在进入模型之前图像应该先被“收拾”一遍Harness 在多模态场景下新增的职责就是对图像输入做前置处理。这一步做得好能直接决定整个 Agent 的稳定性和成本。我的处理管线一般是这样尺寸归一化超过模型支持的输入尺寸时先等比缩放避免模型内部暴力 resize 导致信息丢失。关键区域裁剪如果任务只是“看表格里的库存数”就没必要把整个页面截图都喂进去。可以先做一层对象检测/OCR 定位把表格区域单独切出来再送进模型。OCR 预提取对纯文字密集的截图先跑一遍 OCR 得到结构化文本很多时候比直接让模型读图更准、更省 token。图像压缩与格式转换对非关键区域可降低分辨率或转成 JPEG 压缩格式减少传输体积。我设计了一个多级视觉策略Harness 根据任务类型自动选择场景处理策略效果整页截图语义理解等比缩放到模型支持尺寸直接送原图保真度最高token 消耗最大表格/文档类截图OCR 预提取 表格区域裁剪准确率提升token 下降约 60%UI 自动化点击关键区域裁剪 坐标标注定位更准上下文更短多图对比先单图理解汇总再统一比较避免多图 token 叠加爆炸这套策略落地后库存截图任务的单次图片 token 开销从原来的约 4000 降到了 1200 左右而模型回答准确率反而高了。原因也很简单模型接收的是更干净、更聚焦的信息而不是一整张无关元素堆叠的截图。2.3 成本账单多模态输入到底贵在哪我给团队算过一次账。原先纯文本 Agent 处理一个任务平均消耗约 2000 token接入多模态后单张截图按 4000 token 计再加上完成工具调用需要的多轮推理整个任务轻松破万。按当前主流模型 API 价格粗算单任务成本从几分钱涨到几毛钱翻了 5 到 8 倍。这不是一个可以忽略的数量级。实际工程里我做了三件事来压成本分层缓存。同一张截图如果在短时间内被多次请求Harness 直接复用首次解析的结构化结果不再重复送模型。文本优先。能通过 OCR、日志、接口拿到的文本信息绝不走图像理解只有文本拿不到的关键信息才送图。延迟视觉加载。在任务开始阶段先不加载图片等模型判断“需要看图”时再注入图像 token避免无效的视觉开销。多模态不是一个“开箱即用”的能力它需要 Harness 层面围绕它重新设计数据流。在我目前这套架构里“图”已经不是简单的用户输入而是一类需要被管理、裁剪、缓存、校验的资源对象。3. MCP 的价值不是省掉胶水代码而是把工具变成“可发现的资源”3.1 我为什么从“自写工具层”转向 MCP在做 Agent 的早期我接工具的方式很原始每个工具写一个 Python 函数然后手写一段 JSON Schema 描述参数再在系统提示里拼一段“你可以调用这些工具…”的说明。模型输出一个 JSON我用正则和类型断言去解析。这套东西能跑通但维护起来相当痛苦。新加一个工具要写函数、写 Schema、写 Prompt 片段、写解析逻辑、写异常处理工具多了之后模型经常把 A 工具的字段填到 B 工具上因为描述不够清晰、互相干扰。后来 MCP 出现我花了两周时间把内部工具层全部迁了过去体会很深。MCPModel Context Protocol是某头部 AI 实验室在 2024 年底提出的开放协议设计目标就是标准化“模型应用”与“外部工具/数据源”之间的对接。你可以把它理解成 AI 界的 USB 接口以前每接一个新设备都要专门配一根不兼容的线现在设备统一做成 USB 接口插上就能用。MCP 就是那根统一线。3.2 MCP 的协议设计核心工具发现、调用、资源读取MCP 基于 JSON-RPC 2.0定义了客户端Client运行在 Harness 侧和服务端Server封装具体工具之间的通信方式。最重要的三组能力tools/list客户端向服务端请求“你有哪些工具”服务端返回每个工具的名称、描述、入参 Schema。这是“工具可发现”的根基。tools/call客户端按名称和参数调用具体工具服务端执行后返回结构化结果。resources/read读取服务端暴露的数据资源比如文件内容、数据库查询结果。传输层有两种主流方式传输方式适用场景特点stdio本地子进程服务端与 Harness 同机零网络配置适合本地工具链Streamable HTTPSSE远程服务跨机器调用网络传输适合微服务化的工具集群在 Harness 里MCP 客户端负责维护连接生命周期、调用会话、错误恢复。服务端则只需要实现工具逻辑和 Schema 声明不用关心上层模型怎么使用。这一层解耦带来的直接好处是新接一个工具我不需要动 Harness 主流程只需要新增一个 MCP Server然后用客户端一连接工具就“出现”在注册表里了。3.3 接入一个 MCP Server 的完整过程我用官方 Python SDK 写过一个极简 MCP Server用来暴露两个库存相关工具核心代码如下from mcp.server import Server, stdio_server from mcp.types import Tool, TextContent app Server(inventory-tools) app.list_tools() async def list_tools(): return [ Tool( namequery_stock, description按仓库ID查询当前库存返回SKU列表及剩余数量, inputSchema{ type: object, properties: { warehouse_id: {type: string, description: 仓库ID例如 WH-01} }, required: [warehouse_id] } ), Tool( namecreate_replenish_order, description为指定SKU创建补货申请单, inputSchema{ type: object, properties: { sku_id: {type: string}, warehouse_id: {type: string}, quantity: {type: integer} }, required: [sku_id, warehouse_id, quantity] } ) ] app.call_tool() async def call_tool(name: str, arguments: dict): if name query_stock: result await db.query_stock(arguments[warehouse_id]) return [TextContent(typetext, textjson.dumps(result))] if name create_replenish_order: order_id await db.create_order(arguments) return [TextContent(typetext, textfcreated:{order_id})] raise ValueError(fUnknown tool: {name}) async def main(): async with stdio_server() as (read, write): await app.run(read, write) if __name__ __main__: asyncio.run(main())在 Harness 侧调用方式更简单起一个 MCP 客户端连接到该 Server执行list_tools拿到工具清单转成模型可识别的工具描述注入上下文后续模型若有调用意图直接转发call_tool请求即可。整个过程里Harness 完全不关心服务端内部实现细节这就是统一协议带来的爽感。3.4 “直接写函数”与“MCP”的对比什么时候用哪个我常被问一个问题MCP 相比直接在代码里调用函数有什么本质区别我列个表说明对比维度直接函数调用MCP 接入新增工具成本改 Harness 代码、加 Schema、测全链路写独立 Server客户端自动发现工具描述可见性依赖人工维护 Prompt由 Server 声明动态注入跨语言/跨进程仅限同进程内实现天然支持进程/机器边界可观测性需自建日志协议自带请求/响应结构对模型侧统一性每个工具风格可能不一致统一的工具发现与调用语义但 MCP 也不是银弹。对于延迟敏感、调用极其频繁、且和 Harness 强耦合的内部函数比如内存状态读取、超轻量计算直接函数调用依然是更好的选择。MCP 更适合的是“需要被模型发现和选择”的外部能力尤其是数据库查询、HTTP API、文件操作、第三方服务这类边界清晰的工具。4. 三个组件凑在一起我搭建一个“看图→查库存→创建补货单”Agent 的实测记录4.1 这次我想做什么裁掉理论我直接在本地搭了一个最小可跑的链路目标场景是给 Agent 一张电商后台库存截图它需要自己看出哪些 SKU 库存低于警戒线然后调用库存查询工具核对实时数据最后为缺货 SKU 创建补货单。整条链路涉及多模态模型负责从截图中提取 SKU 列表和库存数。Harness负责任务循环、状态管理、动态工具注入、步骤控制。MCP把“查询库存”和“创建补货单”封装成两个独立工具服务。链路顺序是用户传入截图 → Harness 预处理并构建上下文 → 多模态模型输出结构化数据 → Harness 调用 MCP 客户端 → MCP Server 执行工具并返回结果 → Harness 把结果回填给模型 → 模型决策下一步 → 最终输出。4.2 核心代码Harness 主循环 MCP 客户端接入我先把 Harness 主循环中的多模态处理部分抽出来让大家直观感受一下上下文组装方式async def build_visual_context(image_path: str, ocr_mode: bool True): pil_image preprocess_image(image_path) # 缩放、裁剪 if ocr_mode: text_block await ocr_extract(pil_image) # OCR预提取 return { image_tokens: None, text_block: text_block, visual_summary: 库存表格含SKU、名称、当前库存、警戒值四列 } else: return { image_tokens: pil_image, # 直接走视觉token text_block: None, visual_summary: 原图直接送视觉模型 }async def run(task_text, image_path): state AgentState(task_texttask_text) visual await build_visual_context(image_path) state.history.append({ role: user, image_text: visual[text_block], visual_hint: visual[visual_summary] }) tools await mcp_client.list_tools() # 通过MCP自动发现 context render_with_tools(state, tools) for _ in range(8): resp await vlm.chat(context, toolstools, max_tokens1024) if resp.tool_calls: for call in resp.tool_calls: result await mcp_client.call_tool(call.name, call.arguments) state.history.append({role: tool, name: call.name, result: result}) context render_with_tools(state, tools) continue return resp.content raise TimeoutError(agent steps exceeded)这段代码把三个组件粘在了一起。MCP 客户端负责工具发现和调用模型只负责生成“下一步做什么”的决策Harness 负责循环和控制。跑通这个 Demo 后我有几个很直观的观察观察一模型“看懂”截图 找到正确工具的可靠性比纯文本时代高一个台阶。因为 OCR 预提取已经把表格变成了文本模型不用靠视觉推理去猜数字而是直接在文本上计算准确率明显提升。观察二新增工具的速度确实快了。我后来又给 MCP Server 加了一个“获取仓库列表”的工具Harness 端没改一行代码重新连接后工具自动出现在模型可选择的清单里。观察三日志好查了。每步是“模型请求”还是“工具调用”一目了然。以前出错要翻半天现在直接看 MCP 的 tools/call 入参就能定位。4.3 一个典型执行过程的逐步时间线为了给没有跑过的读者一个具象认知我把一次成功执行的步骤时间线列出来用户上传截图Harness 预处理OCR 提取出 7 行 SKU 数据。多模态模型读取 OCR 文本判断需要调用query_stock工具核对“仓库 WH-01”的实时库存。MCP Server 返回实时库存 JSON模型逐一对比发现 3 个 SKU 低于警戒线。模型生成补货数量调用create_replenish_order创建补货单。服务端返回订单号模型汇总结果输出最终回复。整个过程耗时约 12 秒共 4 次模型请求、3 次工具调用token 总消耗约 8500。这个数据比我预想的要好主要归功于 OCR 预提取把图片 token 压缩掉了。5. 实测中的三个坑与完整排查链路5.1 坑一模型“看得懂图却选不对工具”——级联误差排查这个坑最让人抓狂。现象是模型能从截图中正确识别出“SKU 数量不足”但在调用query_stock时把warehouse_id填成了商品条码导致查询结果为空任务直接失败。我的排查链路分四步第一步看模型原始输出。打印完整 tool_calls JSON确认模型确实打算调用正确的工具只是参数填错了。第二步反查入参 Schema。发现warehouse_id的描述就一句话“仓库ID”没有给示例也没有注明“形如 WH-01”。模型看到截图里既有“仓库A”又有“SKU12345”在字段语义模糊的情况下就会填错。第三步回看 OCR 输出。发现 OCR 文本里“仓库号”那一列的表头识别成了“库存地”表格语义被削弱。第四步修复。我做了两件事一是把工具字段描述改成带枚举和示例的写法明确SKU与仓库ID的含义和格式二是给多模态模型追加一条提示“注意区分仓库ID与SKU编号仓库ID通常以 WH 开头。”修完重测通过率从 62% 提到 95%。这个坑的本质是级联误差多模态理解是第一层误差源工具参数映射是第二层误差源两层一叠加失败率就会被放大。解决办法是拆层收敛每一层都加约束和校验让误差在每层就被拦下来而不是一路传导到最后。5.2 坑二MCP 工具描述太长引发的上下文污染把 50 个工具全接进 MCP 后我发现模型开始频繁选错工具甚至出现幻觉工具名。排查后怀疑是系统提示里的工具描述太长模型在超长上下文里注意力被稀释。简化的验证方法是固定任务只保留任务相关工具比如只留 4 个库存工具重测选对率从 71% 直接升到 93%。这证实了“工具清单越长选择越不准”。我的长期解法是动态工具分组MCP Server 在list_tools之外额外暴露一个list_tool_groups能力Harness 根据任务关键词只加载相关分组的工具。比如“补货”任务只加载库存、订单、仓库三个分组无关的人事、财务工具完全不进入上下文。这样既保留了 MCP 工具可发现的优点又控制了上下文长度。另外给每个工具写描述时前 15 个字就要说明白它是什么详细规则和示例放在后面让模型注意力聚焦在关键信息上。5.3 坑三Harness 中的多模态状态过期问题多模态 Agent 有个隐藏问题图像是静态的但它描述的世界是动态的。我的 Agent 在跑长任务时出现过一种情况模型在第一步看了截图 A认为 SKU 缺货但等它执行完前两个工具调用后实际库存已经因为其他人并发操作而补上了。模型不知道继续按旧截图的数据建补货单造成重复补货。这个问题的根因不在模型而在 Harness 的状态管理设计——图像状态没有“新鲜度”概念。我加了三个机制数据新鲜度标记。图像理解结果附带captured_at时间戳后续工具调用前检查时间差。工具结果强校验。create_replenish_order在创建前先查一次实时库存若实时量已高于警戒线则拒绝创建并返回说明。主动重新观察。模型在重要决策前如果判断环境可能变化会主动触发“重新抓取截图/刷新图像”动作。加了这三层之后类似问题基本被焊死。这里也想提醒大家多模态状态处理不是一次性输入而是一个持续跟踪的资源生命周期问题。6. 一些来自一线的选型决策与路线建议6.1 我目前的选型决策表搭建这套系统时我在几个关键点上做了取舍整理成表供参考选型项我的选择原因与场景多模态模型开源 7B~13B 级 VLM可控、可本地化部署数据不出内网图像预处理深度依赖 OCR 区域裁剪表格/文档场景下准确率高且 token 开销小Harness 实现自研轻量框架团队内部任务固定自研可控性更强MCP 传输层stdio 为主远程用 Streamable HTTP本地工具链为主无额外网络故障点工具注册方式MCP 动态分组加载控制上下文长度避免工具选择漂移如果你没有自研诉求也可以先用社区成熟的 Harness 框架再逐步替换但如果你的任务里有大量私有业务逻辑自研 Harness 的门槛并没有想象中高核心就一个循环加状态容器。6.2 如果让我从头再搭一遍我会按什么顺序走第一遍做的时候我痛苦很大因为一上来就铺了十个工具、接了三个外部系统结果哪里出错都不知道。如果再来一次我会按这个顺序推进第一阶段只跑通“文本进、文本出”的单步模型调用Harness 先不接任何工具。第二阶段加三个简单工具走通“模型选择工具→执行→回填→继续推理”的最小循环。第三阶段把工具封装成 MCP Server验证动态发现和调用。第四阶段加多模态。先只做截图输入 OCR 预提取不急着直接上视觉 token。第五阶段再做工具分组、状态新鲜度、缓存这些进阶能力。每个阶段都有明确的可验证目标出问题时只需要看当前阶段引入的变量定位会快很多。最后分享一个非常个人化的体会多模态、Harness、MCP 这三样东西单独拿出来看都不难难的是把它们组合进一条完整的工作链路里。模型负责聪明Harness 负责稳定MCP 负责整洁三者各司其职之后Agent 才真正开始像一个靠谱的同事而不是一个偶尔灵光、经常出错的玩具。这套架构我还会继续迭代尤其是状态新鲜度校验和工具分组策略这两块后续值得专门再写一篇记录。