ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

多模态Agent架构重构:多模态理解、Harness与MCP工具调用的工程实践

多模态Agent架构重构:多模态理解、Harness与MCP工具调用的工程实践 做Agent开发这两年我一直有个很深的感受真正卡住进度的往往不是模型能力而是模型周围那一圈胶水代码。最近我重构了一个多模态Agent项目的整体架构把输入侧、执行侧、工具侧分别做了改造收获最大的三块是多模态理解的接入方式、Agent执行框架业界常说的Harness的重构以及用MCP统一工具调用的落地实践。这篇笔记就是围绕这三个方向展开的里面记录了我在真实项目里踩过的坑、反复验证过的方案以及最后沉淀下来的代码骨架。如果你正在做LLM应用、智能体类产品或者正准备从纯文本Agent往多模态和协议化工具方向演进这篇内容应该能让你少走一些弯路。我会尽量把为什么这么设计也讲清楚而不是只丢一段能跑的代码。1. 多模态接入的本质把看图能力正式放入Agent决策循环1.1 为什么你的Agent迟早要处理图像先说需求背景。之前我负责的一个内部流程自动化工具最初只处理纯文本格式的异常反馈用户用文字描述问题Agent根据关键词走修复流程。后来业务侧提出一个看似很小的需求让用户直接上传报错截图。结果这个小需求直接把原来的架构戳穿了——纯文本Agent面对截图时既不知道图里是什么也无法判断修复动作是否生效。我一开始的应对方案很粗暴调一个现成的OCR服务把截图里的文字抽出来再走文本Agent。但很快我发现两个问题。OCR只能拿文字拿不到按钮是否变灰弹窗是否出现这类视觉状态而且把截图降级成文字等于把模型最擅长的视觉理解能力扔掉了。后来我换了思路直接让多模态大模型参与Agent的决策循环把图片作为一种与文本平级的输入模态传进去让模型在看的基础上做判断和规划整个能力边界完全不一样了。1.2 多模态输入在Agent里的三个典型用法我把多模态在实际Agent项目里的用法归纳为三类每一类的技术方案和成本都不一样建议你按需选择。第一类是视觉直接理解。用户上传截图模型直接回答图里是什么状态。这类用法最简单把图片转成base64或传URL给视觉模型即可适合报障、审核、内容分类场景。第二类是多模态RAG。当知识库里存在大量含图表、截图的文档时不能只做文本切分要把图片切片单独存放等检索时看是否需要视觉模型二次理解。比如一份操作手册里的点击右上角设置按钮截图如果只靠文本检索这句话背后的图片信息就丢了。第三类是视觉结果校验。这是我后来觉得价值最大、但很多人容易忽略的用法。Agent调用工具执行了一个操作后怎么确认操作真的成功比如点击了某个按钮按钮是否真的切换状态给视觉模型一张执行后的截图让Harness通过视觉描述来做断言。这一步把Agent从盲人操作变成了带眼睛执行可靠性提升明显。1.3 多模态给Prompt策略带来的变化很多做纯文本Agent的同学会低估一件事加了视觉之后不是你原来的Prompt前面加一句请仔细看图就行的。我调试过程中的经验是视觉Agent的Prompt需要把输入来源和任务边界写清楚。比如用户上传的截图是待分析的故障现场不是你需要模仿的目标否则模型可能试图把截图当作模板来生成回复。另一个变化是多模态模型的看图是有代价的——图像输入会转换成视觉token一张高清截图可能占到上下文的一大部分。所以Prompt里必须明确指示模型优先关注图中报错信息所在区域这类局部注意力引导而不是让它整图扫描既省token又减少幻觉。1.4 视觉token的价格陷阱看着便宜用起来贵视觉token这块我单独提一句因为我第一次接入时被账单吓了一跳。很多视觉模型对图像收费的逻辑是基础token 分块token图片按尺寸切成固定大小的patch每个patch计一定token另外还有固定基础开销。一张1024x1024的截图用高细节模式可能要算上千甚至更多视觉token但如果先用压缩策略把图缩到512左右、开启低细节模式token消耗能降一个量级。所以我在项目里加了一道图像预压缩管线用户上传的截图先统一缩放到最长边不超过1024JPEG质量压到85%再根据任务类型决定用高画质还是低画质模式。对于看按钮是否点击成功这种校验类任务低细节完全够用对于阅读图中表格数据这种任务才用高细节。这个策略上线后视觉相关成本下降了大约60%效果基本没变。2. Harness重构Agent执行的骨架必须自己掌控2.1 什么是Harness它到底管哪些事圈内常说的Harness中文语境下可以理解为Agent执行框架或运行时骨架。它不是模型本身也不全是工具层而是介于两者之间、负责把决策循环跑起来的代码架构。具体来说Harness要做的事情包括维护对话历史、构造系统提示词、调用模型获取回复、解析模型返回的工具调用、执行对应工具、把工具结果回填给模型、判断是否达到终止条件、处理异常和重试。很多人会问这些事不是现成的Agent框架都做了吗为什么还要自己重构我的体会是现成框架的问题不在于功能不全而在于控制粒度太粗。你想在某个中间环节插入如果工具结果超过N字符就自动摘要框架很难让你优雅地插进去你想精确控制模型每轮能看多少历史、工具结果保留多少框架的默认策略往往也不符合你的业务。所以对于深度定制场景自研一个轻量Harness非常值得。哪怕你最终用框架也要先理解Harness的各个组成模块否则出了问题只能黑盒调试。2.2 一个能跑通的最小Harness骨架先给你看一个我在模拟项目中用过的极简版Harness核心循环代码量不大但麻雀虽小五脏俱全def run_agent_loop(model, tools, system_prompt, max_iterations8): messages [{role: system, content: system_prompt}] for step in range(max_iterations): response model.chat( messagesmessages, tools[t.schema for t in tools], # 把工具schema传给模型 ) # 情况1模型想调用工具 if response.stop_reason tool_use: messages.append(response.message) for tool_call in response.tool_calls: tool_result execute_tool(tool_call.name, tool_call.args) messages.append({ role: tool, tool_call_id: tool_call.id, content: truncate_tool_result(tool_result, 4000), }) continue # 继续下一轮让模型看到工具结果后再决策 # 情况2模型直接给出最终回答 elif response.stop_reason end_turn: return response.text # 情况3上下文超长或模型调用异常做降级处理 else: messages compact_history(messages) raise MaxIterationError(f超过{max_iterations}轮未结束强制终止)这段代码里我觉得最值得说的是truncate_tool_result这一步。初版Harness里我没做截断直接把完整工具结果塞回messages模型拿到一个几万字符的工具返回后会出现一个经典毛病——反复调用同一个工具仿佛卡死了一样。后来排查发现是因为超长文本把模型注意力拖垮了模型在下一轮里忘记了之前已经调用过该工具。加截断之后这种重复调用问题基本消失。2.3 停止条件和上下文管理Harness的两个隐形坑Harness里最容易出问题的两个地方我单独拎出来讲。第一个是停止条件。max_iterations设太大会导致Agent过度努力明明问题已经解决模型还在不断尝试验证设太小又会导致任务完不成。我的经验是不只依赖轮数计数还要加一个**无进展终止检测**如果连续两轮模型生成的工具调用完全相同参数也一样大概率是陷入死循环直接终止并向用户输出需要人工介入。这个策略比单纯数轮数靠谱得多。第二个是上下文管理。LLM的上下文窗口是固定的但Agent执行过程中产生的对话内容会不断膨胀。我采用的分层策略是系统提示词始终保留工具结果按重要性分为完整保留截断保留摘要保留三级历史对话超过阈值后用一个小模型把早期对话压缩成摘要再放回上下文。你可以在Harness里为每一类消息设置配额避免某一类消息独占窗口。2.4 为什么选轻量自研而不是重型框架最后说说这个争议话题。我并不是反对用现有框架而是认为要分清场景。如果项目以标准工作流为主、工具调用深度不深重型框架开箱即用确实省事。但如果你的Agent有大量非标准动作比如需要动态禁用某个工具、需要在工具结果里做额外检索、需要把多模态截图结果加入上下文上下文做二次判断那重型框架反而成为一种束缚——你花在绕过框架默认策略上的时间远超自己维护一个几百行Harness的时间。我最终选择了自研一个约四百行的Harness配合一套工具注册表。事实证明因为每个环节都自己掌控后面接入MCP和视觉校验时非常顺滑——这是我认为本轮重构里收益最大的一件事。3. MCP把工具从私有API变成标准协议3.1 MCP解决的痛点每一套工具都是一座孤岛做Agent时间久了你会发现一个尴尬现实每接入一个新工具都要写一套新的对接代码。数据库查询写一个函数内部API封装一个接口网页操作封装成另一个模块。每个工具都有自己的鉴权方式、参数规范和错误返回。这个问题的本质是缺少一个统一的工具接入协议。MCPModel Context Protocol就是在这个背景下出现的开放协议你可以把它理解为AI应用的工具USB-C接口——它定义了一个标准方式让大模型应用Host通过统一的客户端去连接外部能力提供方Server无论Server背后是文件系统、数据库、浏览器还是企业内部系统。协议层面它基于JSON-RPC有清晰的请求响应模型开发门槛不高。3.2 MCP的三类原语Tools、Resources、PromptsMCP协议里有三个核心原语容易被混为一谈其实职责完全不同Tools是需要模型决策后主动调用的函数由模型根据工具描述来决定我要不要用Resources是应用可以直接读取的数据资源通常用于静态信息获取不需要模型做复杂的决策判断Prompts则是可复用的提示词模板用于标准化常见任务的启动指令。在我做的内部支持Agent里这三类原语对应得特别清楚Tools对应查询工单状态创建工单回复读取CSV报表Resources对应当前用户的权限范围说明系统的可用功能列表这些是模型在开始回答前就该看到的背景信息Prompts对应新工单首响模板故障升级判断模板。把这三类分清楚后代码组织清晰很多不再是一锅粥。3.3 一个最简MCP Server的落地骨架接入MCP并不是什么大工程。下面是一个我用Python搭建的最简Server骨架实现了两个工具读取文本文件和读取CSV表格。实际项目中你完全可以按同样模式扩展自己的工具import csv import json from mcp.server import Server, NotificationOptions from mcp.server.stdio import run_server server Server(internal_support_server) server.list_tools() async def handle_list_tools(): return [ { name: read_text_file, description: 读取指定路径的文本文件内容, inputSchema: { type: object, properties: { path: {type: string, description: 文件绝对路径} }, required: [path] } }, { name: read_csv_head, description: 读取CSV文件的前N行用于快速了解数据结构, inputSchema: { type: object, properties: { path: {type: string}, n: {type: integer, default: 20} }, required: [path] } } ] server.call_tool() async def handle_call_tool(name: str, arguments: dict): if name read_text_file: with open(arguments[path], r, encodingutf-8) as f: return [{type: text, text: f.read()}] elif name read_csv_head: with open(arguments[path], r, encodingutf-8) as f: rows list(csv.reader(f))[: arguments.get(n, 20)] return [{type: text, text: json.dumps(rows, ensure_asciiFalse)}] else: raise ValueError(f未知工具: {name}) async def main(): async with server: await server.start() await run_server(server)写这个Server的过程中我感觉到MCP最大的价值工具实现与Agent宿主完全解耦。你写好的Server可以被任何支持MCP的Client连接不需要为每个项目重新封装SDK。内部团队要共享某个能力时直接把Server地址或命令发过去就行。3.4 MCP与Function Calling的取舍别搞混了很多刚接触的人会把MCP和Function Calling当成同一个东西其实它们是两码事。Function Calling是某种大模型API提供的一种输出格式——它让模型按特定JSON结构输出来表达我想调用哪个函数、参数是什么本质是模型侧的接口能力MCP是模型应用与外部工具之间的通信协议解决的是工具怎么被描述、怎么被发现、怎么被调用的标准问题。你完全可以在Agent里同时用两者模型侧通过Function Calling输出结构化的调用意图Harness解析这个意图后通过MCP协议转发给对应的Server去执行。这样组合的好处是模型的调用表达是标准化的工具的接入也是标准化的中间的映射逻辑只需要维护一张函数名与MCP工具的对应表即可。4. 一个具体案例截图报障Agent的完整链路4.1 业务场景与架构设计理论讲完了我拿一个从头到尾做完的模拟项目来串一遍。这个项目叫截图报障Agent业务场景是用户上传一张软件报错截图Agent需要自动识别问题类型、查询相关配置、给出处理建议必要时自动执行修复动作并截图验证。整体架构分成四层输入层负责接收图片做预压缩和格式转换理解层用多模态大模型识别截图内容输出结构化描述错误类型、报错代码、界面状态决策层由Harness驱动根据结构化描述决定下一步动作是继续追问、查询资料还是执行修复执行层通过MCP连接多个Server文件读取、API调用、截图服务。这个分层让每层都能独立替换比如今天换一个视觉模型只需要改理解层Harness和执行层完全不动。4.2 关键链路视觉理解结果如何喂给Harness这里有个设计细节很关键视觉模型输出的结构化结果不是直接当作用户消息塞给Harness而是作为一条独立的工具结果消息注入决策循环。我的做法是写了一个describe_screenshot工具内部调用视觉模型把截图转成一段JSON描述Harness拿到这段描述再做下一步规划。这样做的好处是决策模型不用亲自处理图片只需要消费文本形式的视觉结论上下文占用小且视觉模型可以独立替换。我上个真实运行日志的断面给你看。用户上传了一张包含数据库连接超时的报错截图Agent的执行轨迹大致是Step 1: 用户上传截图 Step 2: Harness调用 describe_screenshot得到视觉描述 {error_code: DB_TIMEOUT, service: order_service, ui_state: red_error_page} Step 3: Harness根据视觉结论决定调用 mcp_tool: query_service_config(serviceorder_service) Step 4: 工具返回该服务配置了10秒连接超时上游依赖两个内部API Step 5: Harness判断可能是超时配置过短调用 mcp_tool: search_docs(order_service 超时优化) Step 6: 找到历史优化方案生成处理建议并附上需要人工确认后执行修复这个链路跑起来后最大的感受是视觉模型负责看规划模型负责想MCP负责取各司其职调试定位问题非常清晰。以前混在一起出了问题你很难说是看错了还是想错了现在每一环都有自己的输出日志。4.3 效果对比加了这三件套之后的数据变化我在这个模拟项目上做了一组前后端对比主要观察三个指标任务完成率、平均耗时、上下文Token消耗。改造前用的是纯文本Agent只提取截图文字 硬编码工具调用改造后是视觉理解 自研Harness MCP执行。指标改造前改造后说明首次正确识别率62%91%视觉模型直接理解截图不再依赖OCR降级平均任务完成率修复/建议被采纳54%78%Harness的终止与重试策略减少了半途而废单任务平均Token消耗决策模型约8K约6K视觉结论独立注入避免决策模型看大图新增工具对接平均耗时约1.5天约2小时MCP协议统一了工具描述与调用格式这组数据不算严谨的统计实验但趋势很明显多模态理解提升了信息获取的完整性自研Harness提升了执行过程的可靠性MCP大幅提升了工具接入的效率。三者不是孤立生效的它们共同构成了感知—决策—行动的完整闭环。4.4 这个案例里MCP最让我惊艳的一点最后说一个我当时没想到的好处。某个内部系统原本没有暴露任何API只有一堆手动导出的CSV文件。按照以前的思路要么求对方开API要么让流程彻底放弃自动化。用MCP之后我只写了一个十几行的CSV文件读取Server把这个Server通过MCP协议暴露给Agent问题就解决了。这种把已经有但没有接口的能力低成本地变成Agent工具的场景我认为是MCP在企业和团队内部最有应用价值的方向。5. 上线后踩过的性能与稳定性坑5.1 多模态推理延迟成了新瓶颈改造上线后最先暴露的问题是延迟。纯文本Agent一次决策平均1到2秒视觉Agent单次看图理解可能要到4到8秒。对于一个需要连续看两三次截图的流程总延迟会翻倍。我的处理方式分两层小图用低延迟小模型做快速理解只有理解置信度低时才升级到大模型重新判断同时在Harness里加了视觉结果缓存同一张截图在同一任务中只处理一次避免重复识别。这一套下来端到端平均耗时降了接近40%。5.2 视觉Token的预算管理必须前置前面提到视觉token的策略实际落地时我做成了一张预算控制表Harness在调用视觉工具前先估算场景压缩后尺寸细节模式单次预估Token界面状态校验512x512low约几百Token报错弹窗识别768x768low约几百Token表格数据读取1024维度high约数千Token整页业务文档理解1280维度high约数千至上万TokenHarness里预置了每一类调用允许的最大视觉Token超了就强制降级压缩。我提醒你视觉Token消耗是Agent成本里最容易被低估的一项一定要在做预算时就把它单独列出来否则月底账单会吓你一跳。5.3 MCP连接的复用与超时问题MCP如果走stdio模式本地进程通信每个Agent实例都得拉起一个子进程进程多了会明显感受到资源占用。我遇到过最尴尬的情况是高并发测试时一堆MCP Server进程没有被正常回收机器内存被打满。后来我在Harness层做了统一的连接管理与复用同一类型Server只保留一个连接实例用完后不销毁而是挂起复用同时给所有MCP工具调用加上超时时间默认60秒超时后走快速失败分支把错误信息回传给模型而不是让Agent无限等待。5.4 Harness终止条件和重试策略的最终调优最后说一组调参数据。这个项目的Harness最终把max_iterations定为8轮无进展终止从两轮相同调用调整为两轮相同调用且相同参数才终止。重试策略不是所有工具调用失败都重试对于幂等的读类工具失败自动重试1次对于写类/修复类工具失败后立即停止并通知人工避免自动化误操作放大问题。一个小教训是重试时使用指数退避简单的固定间隔重试在高并发时容易造成服务端雪崩。最开始我图省事用固定1秒重试有一次流量上来系统报错排山倒海而来好几天都有阴影。后来统一改成0.5秒、1.5秒、4秒的三级退避连熔断逻辑都写了整体稳定很多。5.5 可观测性Agent日志设计的一点心得与监控相关的一个小建议不要只记录句子级的日志要按决策循环的Step维度输出结构化日志。我会记录每一步的消息类型用户输入/工具调用/工具结果/模型输出、Token消耗、耗时和状态最后用链路ID把所有Step串起来。这样当用户反馈Agent答错了时你能快速定位到是哪一步的信息不准确而不是面对一坨笼统的日志发呆。这种做法在我看来投入产出比是所有优化里最高的。最后分享一个实操中的体会回头去看这次架构调整我最核心的体会是三句话多模态不是给Agent加一个看图功能而是让感知层真正参与决策循环Harness不是越大越好而是控制粒度越匹配业务越好MCP短期内可能看不到明显收益但它像一层连接标准化的基础设施工具越多、团队越大价值越明显。如果在做类似项目建议别一上来就追求大而全的框架。可以先按这个顺序走一遍用一个多模态模型跑通截图到结构化描述再用一个两三百行的Harness把决策循环转起来最后用MCP把你最常用的两三个内部工具接进去。这套最小闭环能跑通之后再逐步扩展工具面、优化成本和延迟。项目是动态的架构的合理性永远取决于下一个真实需求。
RELATED READING

延伸阅读

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