最近手头的几个 Agent 项目陆续跑起来之后我明显感觉到一个趋势纯粹发一段文字、收一段文字的玩法已经不够用了。业务方提的需求里开始出现帮我看看这张报表截图的数据把这个视频里提到的产品信息整理出来按住这个操作界面走一遍流程这类要求。于是多模态、Harness 和 MCP 这三个词就成了我近一个月的高频检索对象。单看概念它们各自有各自的资料多模态是讲模型怎么理解图片音频Harness 讲 Agent 怎么编排工具和循环MCP 讲怎么标准化的把外部数据源接进模型上下文。但真正把它们放在同一个 Agent 工程里你会发现它们根本不能分开谈。多模态负责让 Agent 看得见Harness 负责让 Agent 跑得稳MCP 负责让 Agent 接得上。三者共同决定了一个真实业务 Agent 的天花板。这篇笔记就是我这段时间的实践记录里面有概念拆解、有代码片段、有踩坑清单。适合两类人看一类是已经写过简单 Agent 原型、正准备往业务场景深挖的开发者另一类是技术管理者想搞明白最近大家都在提的这几个新名词底子里到底在解决什么问题。1. 先捋清三个概念在 Agent 里的分工很多人一上来就陷入某个技术的细节里结果越看越乱。我觉得第一步不是深入某个协议或某段代码而是先把地图画出来在一个 Agent 系统里多模态、Harness、MCP 到底各自管哪一段。1.1 多模态Agent 的感官系统多模态解决的是输入太单一的问题。传统大模型只能吃文本也就是用户说什么你传什么。但真实业务里信息天然是多种形态的客户发来一张截图、扫描件里是表格、视频里有语音和字幕、设计稿里有视觉布局。多模态模型把这些非文本内容转成模型能理解的特征表示让 Agent 有机会自己看一眼。在 Agent 架构里多模态处在最前端承担的是感知层的角色。它决定 Agent 能不能准确理解任务里的图像约束也决定后续工具调用是否正确。比如你要 Agent 根据一张系统截图去判断当前哪个按钮是可用状态模型必须真的能看清按钮位置、颜色、灰度差异。这个环节如果出错后面 Harness 编排得再好也没用因为它的输入已经是错的。1.2 HarnessAgent 的骨架和执行环境Harness 这个词在业界有两个常见的语境。一个是评估框架用来批量跑测试用例给 Agent 打分另一个是运行编排框架用来管理 Agent 的执行流程。我在这篇笔记里主要聊后者因为它在工程化时牵扯的细节更多。Harness 管的是 Agent 的执行骨架拿到用户请求之后怎么拆解、按什么顺序调用模型和工具、循环多少轮、中间结果存在哪里、出错了怎么重试、上下文多长要截断、一轮任务最多烧掉多少 token。说白了模型是大脑Harness 就是身体骨架和神经系统。没有 Harness模型只能被动回答有了 Harness模型才具备反复尝试的能力。我最早写 Agent 时没有 Harness 的概念就是一个 while 循环里反复调模型接口上下文不断拼接直到模型说完成了。这种写法做 Demo 可以一上真实业务就崩溃后面会详细讲。1.3 MCPAgent 接外部世界的标准化插座MCP 全称 Model Context Protocol翻译过来是模型上下文协议。它解决的是工具接入方式太碎片化的问题。在没有 MCP 之前每个 Agent 要接数据库、网盘、在线文档、消息系统都得各自写一套接口函数。每个工具的鉴权方式、参数格式、返回值结构都不一样代码里到处是 if-else 分支。MCP 把这套东西标准化工具统一暴露为工具列表调用统一走一个协议消息格式客户端的接入逻辑只需要写一次。你可以把 MCP 理解成 USB-C 接口以前每个设备都有自己的充电线现在大家都按同一个标准设计接口一套线材通用。MCP 之于 Agent 也是一样它规定了工具提供方Server和工具消费方Client怎么握手、怎么声明能力、怎么传参、怎么返回结果。这让 Agent 在接入新工具时不再需要从零适配私有 API而是直接连接一个符合协议的 Server 就行。1.4 三者的协作关系把这三个角色放到同一个流程里看关系就很清楚用户发来需求可能附着截图或语音多模态模块先把这些非文本内容处理成模型能理解的信息Harness 接收模型输出判断下一步该调什么工具工具调用通过 MCP 这个标准通道发给外部服务外部服务的结果再回流到上下文Harness 决定要不要进入下一轮循环最后模型基于已有信息给出最终回复。任意一环掉链子整个 Agent 都会崩。多模态识别错了后面全错Harness 没有循环边界任务能无限耗下去MCP 没做好鉴权任何使用这个 Agent 的人都能触达核心数据。所以我觉得这三个点不是三个并列的技术选项而是一个整体工程里的三个必修课。2. 多模态接入的实操路径和看清背后的细节多模态这部分我把自己踩过的坑和总结的流程一起写重点讲 Agent 场景里最常用的视觉理解。2.1 Agent 场景里最常见的多模态输入类型我在实际项目里碰到的主要有四类截图最常见比如业务系统的操作界面、报错弹窗、聊天记录。Agent 需要根据截图判断当前状态甚至模拟操作。扫描件/PDF表单、合同、发票文本和表格混排模型需要同时做版面分析和内容提取。图片中的文字广告图、海报、设计稿纯英文或纯中文都还算简单中英混排、艺术字难度就上来了。音频/视频客服录音、产品介绍视频需要转写之后再进文本流程。这类通常不是直接丢给模型而是先做转写再交给 Agent 处理。每种输入的预处理方式不太一样但核心原则是别把原始文件直接甩给模型先想清楚模型到底需要看什么。2.2 视觉理解任务的通用流程以我做得最多的截图理解 后续工具调用为例流程大概是四步图片预处理压缩分辨率、裁剪无关区域、必要时转成 RGB 格式。很多模型对图片尺寸有上限超过上限会被等比缩小导致小字看不清。图片编码把图片交给多模态模型的视觉编码器模型会把图像切成若干个小块每个块转成特征向量。这个小块的分辨率理念很关键它决定了模型能不能看清图里的小字。推理生成模型根据视觉特征和文本提示输出对图像内容的描述、结构化数据或操作指令。结构化校验这一步很多人会漏掉。模型输出的是自然语言你要拿它去触发工具调用就必须从文本中抽出参数、校验格式。比如模型说这个按钮是灰色不可用状态Harness 需要把这个判断映射成一个布尔值再决定分支走向。2.3 一个关键的细节图片分块和分辨率这是我觉得视觉理解里最值得单独拎出来讲的一点。多数多模态模型并不是把整张图作为一个大输入直接处理而是把图像切成固定大小的块每个块独立编码之后再融合。这意味着图片里的文字如果横跨在两个分块边界模型可能会丢失局部信息。实操里的应对办法是在丢给模型之前把图片按目标模型建议的规则预处理。一般我会先做两件事。第一把图片缩放到合适的短边长度比如 768px 或 1024px具体看模型支持区间。不要盲目使用原图的高分辨率很多情况下超高分辨率不会带来精度提升反而增加计算量和 token 消耗。第二把图片里的关键区域适当放大。比如一张截图里我需要模型关注的是右下角某个表格那就先把这块区域裁剪出来再识别而不是把整张大图直接扔过去。实测下来裁剪后的识别准确率能提升很多。2.4 多模态输出不可靠的三个典型时刻依赖多模态模型时你要接受它偶尔看不清或看错。我总结过三个高频翻车现场表格里的数字被认错尤其是手写体或低分辨率扫描件多一个少一个零是常事。模型脑补图片里不存在的内容比如截图里明明只有三个按钮它非说还有第四个。多张图混在一起时模型弄不清哪张图对应哪个说明。所以我的建议很简单凡是多模态输出要驱动下一步操作的地方必须加校验层。校验层可以是规则比如颜色值必须在预设集合里金额必须是数字类型也可以是二次提示让模型自我纠错。多模态的容错不应该靠运气而应该靠工程手段。3. HarnessAgent 运行框架怎么搭才不容易翻车这一节内容是我觉得整个笔记里最经验向的部分。Harness 没有统一的教科书答案但有一些通用的设计取舍我分享自己摸索出来的方案。3.1 Harness 到底在管哪些事一个可落地的 Harness 至少要管住下面几件事任务入口接收用户的原始请求附带多模态内容还是纯文本。上下文管理维护模型对话历史控制长度避免上下文爆炸。工具注册让模型知道当前环境里有哪些工具可用以及每个工具的输入输出规范。执行循环决定什么时候该调模型、什么时候该调工具、什么时候该结束。错误处理工具超时、模型吐格式不对、外部接口限流这些都要有兜底。预算控制一轮任务最多几轮循环、最多消耗多少 token超过就强制熔断。很多入门级的 Agent 框架把这些都埋在里面了你只管写工具函数。但一旦你想做点定制逻辑不深入理解 Harness 的设计很容易在框架的默认行为上碰壁。3.2 从死循环到有界循环我最早写的伪代码是这样的messages [{role: user, content: 帮我查一下上个月的订单量}] while True: response model_completion(messages) messages.append(response) if response.has_tool_calls(): result execute_tool(response.tool_calls) messages.append({role: tool, content: result}) else: break这段代码看着没毛病但真实跑起来会出几个问题。最大的问题是模型可能永远不返回最终回答它可能不断调用工具、把中间结果塞回上下文、继续调用直到把上下文撑爆。有一次我的演示项目跑到第 47 轮都没停最后是因为接口报错才中断的。后来我在 Harness 里加了三样东西最大轮数限制默认 15 轮超过直接终止并把中间结论返回给用户。上下文长度上限按字符数或 token 数双阈值判断超了就触发摘要压缩。token 预算控制每次调用前估一下剩余额度不够就提前收尾。有了这三个刹车Agent 的执行才算真正可控。3.3 可观测性给 Harness 装上仪表盘Harness 不只是让 Agent 跑完还要让你知道它为什么这么跑。我经历过最痛苦的一次调试是用户反馈 Agent 回答错了但我完全不知道它是基于哪条工具返回结果得出这个结论的。没有链路追踪排查问题只能靠猜。因此我后来的 Harness 强制加上了分层日志每一轮循环都记录模型输入消息摘要包含当前多少条消息、大概多少 token模型输出的原始内容包括工具调用的参数工具执行的耗时、返回结果的关键片段上下文压缩或截断的触发原因。把这些信息落成日志之后配合时间戳和 TraceID我基本可以还原 Agent 的每一次决策链。这个东西在业务上线后格外重要因为你要向业务方解释这个自动流程是怎么得出结论的总不能说大模型就是这么想的。3.4 Harness 里的状态持久化早期我天真地认为Agent 的所有状态都存在对话消息里就够了。后来发现不行任务执行到一半进程重启了所有中间状态全丢了。用户得从头再来这在正式产品里不能忍。现在我会把 Harness 的状态分层存储短期状态一轮对话里的临时变量放内存或 Redis 都行注意过期时间。任务级状态整个 Agent 任务的计划、已完成步骤、工具调用记录放数据库带任务 ID。会话级状态跨任务的用户偏好、历史摘要、授权信息单独存储。状态持久化之后Agent 至少能做到断点续跑即使某次执行中断也能从数据库里的任务快照恢复。当然这带来额外复杂度但如果你做的是严肃业务这步不能省。3.5 一个推荐的最小 Harness 结构下面这个结构是我自己项目里的简化版不依赖任何框架方便看清楚原理class AgentHarness: def __init__(self, max_rounds15, max_tokens8000): self.messages [] self.rounds 0 self.max_rounds max_rounds self.max_tokens max_tokens def add_user_message(self, content): self.messages.append({role: user, content: content}) def step(self): response self.call_model(self.messages) self.rounds 1 if self.rounds self.max_rounds: raise RuntimeError(超过最大轮数) if response.is_tool_call: tool_result self.execute_tool(response.tool_call) self.messages.append({ role: tool, name: response.tool_call.name, content: tool_result }) return continue else: self.messages.append({role: assistant, content: response.content}) return done def run(self): while True: status self.step() if status done: return self.messages[-1][content]这个结构虽然简单却把循环、轮数、工具调用、状态存储都串起来了。你在它之上加日志、加摘要、加并发控制就是一个能撑住真实业务的最小骨架。4. MCP把工具调用从私有 API变成标准接口MCP 这个协议我一开始也是只停留在概念层直到我真把两个不同系统接进 Agent 时才意识到它的价值不是减少代码量而是把接入方式从点对点变成一对多。4.1 MCP 的核心抽象工具、资源、提示MCP 定义了三个核心概念我在项目里主要用到的是工具这一块但另外两个也要理解。工具Tool可被模型调用的函数比如查询数据库、发送消息、操作日历。工具面向的是动作。资源Resource可被读取的数据比如一个文档、一份配置、一张图片。资源面向的是内容。提示Prompt可复用的提示词模板比如总结周报生成复盘这是给模型用的预设上下文。在协议层面最重要的消息就是工具的发现和调用。客户端先向服务器问你有哪些工具服务器返回工具列表和参数 schema客户端再发起调用某个工具服务器执行并把结果返回。整个过程采用统一的 JSON-RPC 消息格式。4.2 一个最小 MCP Server 实现为了让不熟悉协议的人有个直观印象我写一个最简的 MCP Server 示例用标准库实现重点是展示消息交换过程。实际生产环境里一般直接用官方 SDK会更省事但理解原理最重要。import json from http.server import BaseHTTPRequestHandler, HTTPServer class MCPHandler(BaseHTTPRequestHandler): def do_POST(self): body self.rfile.read(int(self.headers.get(Content-Length, 0))) request json.loads(body) method request.get(method) if method tools/list: response { jsonrpc: 2.0, id: request.get(id), result: { tools: [ { name: query_sales_data, description: 查询指定月份的销售金额, inputSchema: { type: object, properties: { month: {type: string} }, required: [month] } } ] } } elif method tools/call: params request.get(params, {}) tool_name params.get(name) arguments params.get(arguments, {}) if tool_name query_sales_data: result {month: arguments.get(month), totalAmount: 128000} response { jsonrpc: 2.0, id: request.get(id), result: {content: [{type: text, text: json.dumps(result)}]} } else: response { jsonrpc: 2.0, id: request.get(id), error: {code: -32601, message: unknown tool} } else: response { jsonrpc: 2.0, id: request.get(id), error: {code: -32601, message: method not found} } data json.dumps(response).encode() self.send_response(200) self.send_header(Content-Type, application/json) self.send_header(Content-Length, str(len(data))) self.end_headers() self.wfile.write(data) HTTPServer((127.0.0.1, 8000), MCPHandler).serve_forever()这个示例演示了两件事一是服务器如何向客户端介绍自己有哪些工具二是服务器如何接收并执行工具调用。真实的 MCP 实现会在传输层上做更多工作比如基于标准输入输出流、SSE、或者带鉴权的 HTTP 传输但核心思想是一致的。4.3 从私有实现迁移到 MCP 的实际收益我之前有一个工具模块是写死的函数比如有一个get_sales_data(month: str) - dict函数直接被我 import 进 Agent 代码里。看起来简单但问题在于当第二个 Agent 系统想复用这个能力时它没法直接 import 那个模块因为语言环境不一样、部署位置不一样、鉴权方式也不一样。于是就只能复制粘贴代码或者写一层丑陋的 HTTP 适配器。后来我按 MCP 把查询能力拆成了一个独立服务Agent 端不再直接调用函数而是通过 MCP 客户端去连接服务器。改完之后最直接的变化是新的 Agent 接入时只需要配置一下 MCP 服务器的地址重新拉取一下工具列表就能直接用上查询能力代码几乎零改动。这个迁移的收益不是代码变少了而是边界变清楚了。数据查询服务只负责通过 MCP 暴露自己的能力和参数格式Agent 只负责通过 MCP 发现能力和发起调用。两边不再互相耦合在具体的函数签名里。4.4 MCP 的安全边界该锁死的地方必须锁死MCP 接入外部工具时有一个容易被忽视的隐患模型拿到工具列表后可能尝试调用它不应该调用的工具或者用一些危险参数去调。所以安全控制不能只靠模型自律必须放到服务器这层强制约束。我会在 MCP Server 上做三件事参数白名单对于查询类工具只允许固定字段入参其他参数一律拒绝。权限范围每个 MCP 客户端连接时带上身份标识服务端按标识判断可用的工具子集。未登录的只能读公开数据登录的才能写。操作审计每次工具调用都落日志记录调用者、参数、返回状态。不仅方便排查还能在异常时追溯。在真实业务里MCP 安全比功能更先要命。你肯定不想让一个 Agent 拿着数据库查询权限因为一次 prompt 注入就被人指挥着把整张表导走。5. 三个技术点拼在一起一个报表解析 Agent 的改造实录光讲理论不够我结合一个具体的项目把三个技术点串起来做一个能看懂业务报表图片并执行查询的 Agent。5.1 业务场景和原始痛点业务方给的需求很直接市场部的同事经常在群里发一张后台报表截图然后问这个数对上个月怎么变化这么多。他们希望有一个机器人能自己看截图、识别出报表里的关键指标、再自己到数据后台核实一下趋势最后回复一段解释性结论。这需求最开始的实现很笨人工把截图里的数字抄进一个表格再手工查询数据库。我们想要的是 End-to-End 的 Agent能自动完成看图 — 提取查询条件 — 查数 — 对比 — 回复全流程。5.2 整体设计多模态进Harness 控MCP 出最终落地的架构分为三层输入层多模态模型负责解析截图输出一个结构化的查询意图包含报表类型、月份、指标名称。控制层Harness 接收这个结构化意图拆成子任务按依赖顺序排好。先校验关键参数是否完整再决定调用哪个数据服务。工具层数据查询能力通过 MCP Server 暴露Harness 里的 Agent 通过 MCP 客户端连接发起查询并拿到返回结果。这个设计最关键的一点是多模态模型不直接查数据库它只负责看和提取查数据由 MCP 控制的正式工具完成。这样即使模型看图看错了也只是提取出的参数不对不会真的让模型直接摸到数据。5.3 关键实现片段查询意图提取和工具调用查询意图提取这部分我给多模态模型的提示词大约是这么设计的你是报表理解助手。我会给你一张截图请提取报表类型、统计月份、主要指标并按 JSON 格式输出。只输出 JSON 本体不要解释。 如果截图里没有明确月份请将月份设为 null。模型返回的 JSON 示例{ report_type: sales_summary, month: 2025-03, metric: total_amount }接下来 Harness 收到这个 JSON会先做一次校验。校验规则包括report_type 是否在支持的报表类型列表里month 是否符合 YYYY-MM 格式metric 是否属于已知指标名。只有校验通过Harness 才会继续。这步很值得坚持因为模型输出的 JSON 可能会带上多余字符、字段名拼错甚至类型不对不校验就发出去大概率会引发工具端报错。工具调用阶段Harness 通过 MCP 客户端发出 tools/call 请求{ method: tools/call, params: { name: query_sales_summary, arguments: { month: 2025-03, metric: total_amount } } }MCP Server 返回结果之后Harness 把结果追加到上下文再由模型生成最终的用户答复。答复里不仅要有数值还要解释环比变化、变化可能的原因、建议后续操作。为了让模型解释得靠谱我会额外给它一段同期的历史数据作为参考。5.4 实测结果和参数选择这个流程跑通后我用 50 张真实报表截图做了验证。第一版的效果挺惨准确率只有七成左右问题集中在两处一是截图里有多个表格模型抓错主要指标二是月份信息识别不准会漏掉3月前面的2025。后来做了三处调整准确率提到了九成以上识别前先做表格定位让模型只关注截图里最大区域的表格减少干扰。月份字段增加二次校验模型如果输出 null则启用一个正则规则从图片周围的文本里提取。多模态模型输出后加一个确认对话模型先列出自己从截图里看到的关键信息再由一个判断逻辑决定是否进入正式查询。这一步消耗一些额外 token但能明显减少错误查询。参数上我最后把最大循环轮数设成了 10上下文长度上限设成 6000 token 左右。视觉模型调用时图片分辨率我控制在 1024px 短边既不丢细节也不浪费 token。6. 常见问题与排查技巧实录无论多模态、Harness 还是 MCP实际用起来总会遇到各种各样的怪问题。我把高频问题整理成速查表顺便补充点排查心得。6.1 多模态相关的高频问题视觉理解翻车最常见的就是看不清和看错。我的排查顺序一般是这样先看原始图片分辨率如果短边低于 512px小字很容易糊。再看模型提示词里是否要求输出结构化格式没有的话模型可能自由发挥把结果埋没在废话里。然后看是否裁剪掉无关区域截图里的导航栏、空白区域会分散注意力给识别带来噪声。最后再考虑换更强的多模态模型。另外图片太大的问题也经常遇到。有些模型对单张图片的 token 消耗很高一张高分辨率大图可能占掉几千 token。我的经验是先统一压到合理尺寸不够清晰再局部放大而不是一开始就用最大分辨率。6.2 Harness 相关的排查思路Harness 最容易出的问题是跑着跑着不动了或者跑着跑着失控了。我会优先看三点循环是否到了最大轮数日志里有没有 max round exceeded 字样。如果经常触发说明任务拆解不够细模型在反复尝试同一个无效动作。上下文是否接近上限。上下文太满时模型会丢失早期信息表现为答案前后矛盾甚至开始重复调用同一个工具。工具调用是否出现脏数据。某个工具返回了异常格式模型没法解析就会卡在重试循环里。还有一个我犯过的低级错误把工具返回的超长文本直接塞进上下文导致后续模型响应越来越慢。后来我改了规则工具返回结果如果太长只保留前 2000 字符并在前面加摘要。6.3 MCP 连接常见问题MCP 连接层的问题大部分集中在工具列表拉不到和工具调用超时。工具列表拉不到我一般检查三处服务器是否真的起来、配置的端点路径是否正确、鉴权 token 是否过期。MCP 客户端发现工具列表时如果服务器返回空列表最常见的原因就是服务器内部异常被静默吞掉了导致注册表没初始化。处理办法是在服务器启动时主动打印工具注册结果。工具调用超时要区分是网络慢还是执行慢。我遇到过工具本身要跑 30 秒的报表计算但 MCP 客户端默认超时只有 10 秒结果必挂。这种问题要把超时时间调大或者把工具改成异步任务模式先返回一个任务 ID再通过轮询获取最终结果。6.4 通用排查速查表问题现象可能原因先查哪里多模态识别数字不对图片分辨率低 / 表格混排检查输入图片尺寸和裁剪区域多模态回答偏离事实模型产生幻觉加结构化输出提示和二次校验Agent 循环不终止没有最大轮数限制看 Harness 日志里轮数是否超限上下文过长导致响应慢工具返回内容太大检查工具结果是否做了截断或摘要MCP 工具列表为空Server 内部异常 / 注册失败看 Server 启动日志和注册信息MCP 工具调用超时超时时间配置不合理调整客户端超时或改异步模式工具调用参数错误模型生成的参数不合法在 Harness 层加参数校验规则这张表不是标准答案但它是我排查问题时的第一反应省了不少时间。遇到问题先别急着改模型先判断问题出在感知层、控制层还是接入层再对症下药效率会高很多。最后再分享一点体会。这三个技术方向里最容易学的是多模态的 API 调法最难的是 Harness 的设计取舍而 MCP 值得长期投入因为它在一点点改变 Agent 接入外部系统的规格。我现在的习惯是每接手一个新 Agent 项目都先问自己三个问题模型需要看懂什么、执行流程怎么画边界、外部能力用什么协议接进来。把这三个问题想清楚项目基本不会跑偏。后面如果我再碰到有意思的坑再来更新这份笔记。
阅读完成 · 觉得有帮助?