大模型 Agent 开发做到这个阶段,我最大的体会是:多模态接入、Harness(运行底盘)和 MCP(工具协议)这三件事,每一件单独拎出来都能讲一堆技术点,但真正决定系统能不能稳定跑起来的,是它们之间怎么咬合。模型再强,工具协议再标准,只要 Harness 设计得不合理,任务一样会挂在半路;多模态接入做得再花哨,如果消息结构和上下文管理不匹配,后面所有环节都会跟着遭殃。这篇笔记就是我从实践里整理出来的,关于这三块内容怎么理解、怎么选型、怎么落地的一些思考,以及踩过坑之后沉淀下来的排查方法。适合正在做 Agent 开发、尤其是想让自己的 Agent 具备多模态感知能力并接入外部工具的开发者参考。
1. 多模态 Agent:先想清楚“接入什么”再动手
1.1 多模态不是越多越好:先按场景决定模态优先级
我见过不少项目,一开始就把图片、音频、视频、PDF 全接了进来,恨不得让模型“耳听六路、眼观八方”,结果跑起来之后问题接踵而至:上下文窗口被塞爆、识别结果不准确、延迟和成本直线上升。多模态能力是一种资源,不是什么场景都值得用,更不是一次全上才算“能力强”。
真实业务里,多模态的需求基本可以按这几类来拆:第一类是看图理解,比如截图、照片、表格图片、UI 界面图,这类需求靠视觉语言模型就能解决,关键是要处理好图片的分辨率和信息密度;第二类是文档类,比如 PDF、Word、扫描件,这类大多数情况更适合走“先解析再喂文本”的路线,而不是直接把整页图丢给模型;第三类是音频和视频,这类数据量最大,通常需要先做转写、分段、抽帧,再让模型对关键片段做理解,直接端到端处理大段音视频是不现实的。
实操层面,我建议做一个简单的模态接入优先级表格,把业务场景、期望输入、推荐处理链路、延迟预算和成本关注点一次性列清楚。就拿我最近做的一个内部知识库项目来说,用户上传了很多带截图的故障排查手册,我们最初想直接用多模态模型看图识别,但后来发现这些截图中有大量小号正文和代码片段,VLM 直接读的效果并不好。后来改成先走 OCR 和版面分析,把文字提取出来再喂给模型,准确率反而高了很多。这个转变的核心逻辑是:多模态模型不是“万能读图器”,它对版面和文字细节的把控,并没有专门解析管线那么可靠。
多模态接入的优先级,应当遵循“低频且信息密度高”的场景优先接入,“高频且可结构化”的场景优先做解析,“超大体积”的场景优先做分段和降采样。这样才不会让系统被多模态数据拖垮。
1.2 多模态数据如何进 Harness:统一消息结构是关键
多模态数据一旦进入 Agent 的流程,就必须考虑消息结构的统一问题。早期我在做原型时,图像直接被塞成一个字段放在用户输入里,工具处理音频又用了另一个数据结构,Harness 里的各环节如果要同时处理文本和图像,就要写一堆 if-else 判断,代码越来越难维护,排查问题更是灾难。
后来我参考了业界比较成熟的对话消息格式,把所有模态统一成“消息 = 角色 + 内容列表”的结构,其中内容列表每一项都有明确的类型和元数据。比如一条用户消息,可能是文本和图片的混合,那它就是一个数组,其中一项是文本内容,另一项是图片内容,图片内容里包含图片的 MIME 类型、编码方式和来源标识。这套结构的好处是,Harness 的各个模块只需要识别类型字段,就能决定走文本上下文、走视觉模型,还是先触发解析工具。
有个细节容易忽略:图片 Base64 编码会把体积放大不少,如果一张图几 MB,Base64 之后可能变成几 MB 甚至更大,直接塞进消息里不仅浪费 token,还会拖慢整个流程。我现在的做法是,统一消息结构里的图片字段只保存引用和压缩预览,需要真正送进 VLM 时再按需加载原始数据。另外,在统一消息结构的最外层,我还会加一个处理状态字段,记录这条多模态消息是否已经过解析、是否已生成文本转述、是否需要人工介入。这样 Harness 里每个环节都能快速判断“这条消息到我这步该怎么走”,不至于重复处理同一份多模态数据。
统一消息结构,本质上是在为系统的后续扩展做铺垫。今天你想接入图片,明天要接入音频、表格、网页截图,只要所有模态都按照同一套协议进入系统,Harness 和 MCP 工具就不用为每种新模态改动核心逻辑,这是多模态 Agent 工程化的重要基础。
1.3 同异步取舍与成本控制:看清瓶颈在哪儿
多模态处理有一个明显特征:耗时通常比纯文本长。一张图送进 VLM,慢的时候可能要好几秒甚至十几秒;一段音频要转写,更是分钟级起步。如果 Agent 的主流程是同步调用,那用户就会觉得系统“卡住了”。但异步化也不是万能药,异步会让交互流程变成“给个任务,之后回来查结果”,有些业务场景根本等不了。
我常用的判断标准是:如果多模态处理结果会影响后续工具调用顺序,那它就是主流程的一部分,必须做同步等待,但要加超时和降级;如果多模态结果只是辅助信息,不阻塞主流程,那就可以走异步预取,比如用户刚上传图片时就提前做一次 OCR 或描述生成,等模型真的需要时直接拿缓存结果。
成本控制上,有几个策略实测最有效。一是分辨率降采样,不是所有图都要用原图跑 VLM,很多场景下 512x512 足够;二是 Prompt 缓存,同一张图在同一个会话里经常会被反复提及,如果底层模型支持视觉内容的缓存,能省下不少 token;三是先用轻量模型做粗筛,比如先用一个便宜的模型判断“这张图里有没有关键信息”,有再调更强更贵的模型做细粒度理解。这套“先粗筛、再精读”的思路,在多模态 Agent 里非常值得推广,尤其适合那些用户会频繁上传图片的业务场景。
我试过在某个项目里直接让 Agent 对每张截图都调用最强模型,一个月下来账单涨了三倍多,后来改成先做模糊判断、再按需调用强模型,成本立刻下来了,而且用户感知到的响应速度反而更快。多模态处理不是模型越强越好,而是在合适的环节用合适的能力。
2. Harness 的本质:Agent 的“运行底盘”而不是“调用循环”
2.1 为什么简单循环撑不住真实任务
很多入门教程会把 Agent 的实现描述成一个循环:接收输入,调用模型,拿到结果,如果结果是工具调用就执行工具,再把工具结果喂回模型,直到模型给出最终答案。这听起来很美好,但真实任务中很快会发现漏洞。
第一个漏洞是上下文管理。模型上下文窗口是有限的,多轮工具调用会产生大量中间结果,如果全部塞给模型,几轮之后就会超出窗口。简单循环通常没有压缩策略,只会“越长越卡”,直到报错。第二个漏洞是错误处理。工具执行可能会超时、返回异常、格式不合法,简单循环遇到错误要么直接中断,要么把原始错误一股脑丢给模型,模型往往被一堆乱码带偏。第三个漏洞是并行和调度。真实 Agent 经常需要同时调多个工具,或者一个工具的结果触发另一个工具,严格串行的循环模式根本无法高效处理这类任务。
真正能承担复杂任务的 Harness,应当具备以下能力:状态维护(记住任务走到哪一步)、上下文管理(决定哪些信息该进模型、哪些该压缩)、工具调度(支持串行、并行、条件分支)、错误恢复(失败了能重试或换方案)和可观测性(每一步都能追踪、排查)。它不是一个“循环”,而是一整套围绕任务执行的运行时环境。
我在实际项目里最常用的一种设计,是把 Harness 拆成“状态机 + 事件总线”的模式。状态机管任务的阶段流转,比如“正在理解用户需求”“正在收集信息”“正在执行工具”“正在生成答复”,每个状态对应不同的处理策略;事件总线管模块之间的消息传递,比如图像识别完成、工具执行成功、模型请求超时,都作为事件发布出去,由对应模块响应。这样各个模块之间解耦,新增一个功能时不用把主流程改得面目全非。
2.2 Harness 的五个核心模块与它们的职责边界
我在多个项目里反复调整之后,整理了 Harness 的五个核心模块,这里做个简单梳理:
第一个是调度核心(Orchestrator),它负责解释模型输出,把“该调用哪些工具、按什么顺序调用”翻译成可执行的任务图。我不建议直接在 Orchestrator 里写工具调用逻辑,那样会让调度代码和业务代码纠缠在一起。第二个是工具执行器(Tool Executor),它负责真正调用外部工具,处理超时、重试、并发控制。工具执行器很适合用线程池或协程池实现,但要注意池的大小和任务队列的容量,这个我在后文的踩坑实录里会细说。第三个是上下文管理器(Context Manager),它决定哪些内容要放进模型请求、哪些内容要压缩、哪些内容要丢掉。它同时负责处理“记忆”和历史消息的修剪。第四个是策略注入模块(Policy Injector),它的作用是把系统提示词、工具使用规范、安全约束动态地注入到模型请求里。策略注入要区分“每次都要带的固定策略”和“只在特定任务阶段才注入的临时策略”,避免提示词越来越臃肿。第五个是观测与日志模块(Observability),它要记录每次模型请求的 token 消耗、每次工具调用的耗时、每个决策分支的路径。没有观测模块的 Harness,排查问题等于摸黑走路。
这五个模块在代码上可以各自独立,但它们之间的交互逻辑要提前想清楚。比如上下文管理器和策略注入模块之间经常有冲突:上下文要瘦身,策略注入又要加内容,如果顺序不对,模型看到的信息可能既缺关键上下文,又多了一堆无关指令。我现在的做法是“先策略、后压缩”,系统先把固定策略和临时策略合并成一个策略包,再对历史消息和工具结果做压缩,最后把策略包放在模型请求的最前面,确保优先级。这个顺序问题看起来小,但直接影响模型输出的稳定性。
2.3 状态管理的坑:记忆体不是缓存
状态管理是 Harness 里面最容易被低估的模块。很多人把状态管理等同于“把历史消息存到一个数组里”,认为每次请求都把数组里的消息发出去就行。但这会在真实场景中遇到很多问题。
第一个问题是短期记忆和长期记忆的区分。在单轮任务里,模型只需要知道当前这个任务的上下文;但在多轮对话里,用户会隐含地引用之前聊过的内容。如果 Harness 把所有消息都当成短期记忆,上下文窗口很快被占满;如果过早地把关键信息丢进压缩摘要,又可能丢失重要细节。我的做法是维护两级记忆:任务级记忆保存当前任务链上的关键信息,包括用户的意图、已经执行的工具结果、当前待办步骤;会话级记忆保存跨任务的摘要性信息,比如用户偏好、已确认的约束条件。任务级记忆要完整、可回溯,会话级记忆要精炼、可更新。
第二个问题是记忆和缓存的混淆。缓存是为了避免重复计算而保存的临时结果,比如同一张图的识别结果;记忆是模型在决策时必须依赖的“已知信息”。如果把缓存当记忆用,比如让模型依赖一份过期的工具结果做决策,那输出质量会很差;如果把记忆当缓存用,比如把不该被模型看到的中间日志放进历史消息,那模型会被噪声干扰。我踩过最典型的一次坑,是把所有工具执行的原始返回内容全部塞进记忆,结果模型在后续决策时不断引用一些无关的调试信息,导致最后生成的答案完全跑偏。后来我在工具执行器里加了“工具结果摘要器”,每个工具返回后先自动生成一份结构化摘要,只有摘要进入上下文,原始返回存进观测日志。
第三个问题是上下文压缩策略不能一刀切。有些框架提供默认的消息截断或者摘要功能,但在多模态场景下,直接丢弃较早的图片内容可能导致后续引用失效。我的处理原则是:压缩前先判断当前任务是否还依赖旧的多模态内容,如果依赖,就要保留摘要版引用;如果不依赖,才可以安全移除。这个判断逻辑可以写成一个独立函数,由上下文管理器在每次请求前调用。
3. MCP 落地笔记:协议带来的不只是解耦
3.1 为什么工具要协议化:从硬编码调用到标准协议
在没有使用 MCP 之前,Agent 调用工具的方式通常是在代码里写死:定义一堆函数,由模型决定调用哪个函数名,执行完再拼回结果。这种模式在工具数量少、调用方固定的时候还能凑合,一旦工具数量超过十个,或者同一个工具要被多个 Agent 客户端复用,问题就来了:每个客户端都要知道每个工具的入参格式,工具一改动,所有调用方都要跟着改;鉴权逻辑散落在各工具代码里,安全策略很难统一。
MCP(Model Context Protocol)的思路是把工具调用变成一种标准协议:Agent 端作为 Host,通过统一的协议向 Server 请求工具列表、获取工具描述、发起工具调用。工具的具体实现封装在 Server 内部,Host 不需要关心它怎么实现、用的是什么语言、部署在哪里。这种协议化设计带来的核心价值有两个:一是解耦,工具的提供者和使用者不再需要共享代码库,只要双方遵循同一套协议即可;二是结构化,工具的描述、参数、鉴权方式都有了标准格式,模型更容易理解,系统也更容易做权限控制。
从我自己的实践看,MCP 带来的最大体感不是“代码变少了”,而是“新增工具变得特别轻松”。我之前维护一个 Agent 系统,每接入一个新外部功能都要改 Host 代码、配鉴权、写适配层,流程走完至少半天。现在如果对方提供 MCP Server,我只需要在注册中心配一下地址和权限,几分钟就能让 Agent 用上这个工具。这个效率提升,对工具生态的建设来说意义很大。
3.2 Host 与 Server:谁该干脏活
MCP 协议里有两个关键角色:Host 和 Server。但很多初学者会把它们理解成“请求方”和“处理方”,这样就忽略了它们之间的职责划分。我个人认为,两者还有一个更重要的分工维度:脏活累活应该由谁承担?
Host 侧的脏活主要包括:工具选择的决策、调用参数的组装、鉴权令牌的获取与刷新。这些逻辑本质上是 Agent 编排的一部分,比如模型说“我想查询天气”,Host 要知道该路由到哪个 weather 工具,并把模型给出的参数映射成工具要求的格式。Server 侧的脏活主要包括:工具的具体执行、出错时的错误码和错误消息定义、工具的元数据描述。Server 应该把自己管理的数据和权限边界守好,不该把 Host 的编排逻辑散落到工具实现里。
我遇到过一种反模式:Server 接口设计得特别复杂,把很多业务规则塞进参数里,Host 在调用前要写一堆逻辑去计算这些参数。这看起来“功能完整”,实际却让 Host 和 Server 的耦合度极高。正确做法是 Server 只暴露原子能力,复杂编排交给 Host 或者一个中间编排服务。比如说,与其设计一个“发布文章并通知所有订阅者”的工具,不如提供两个工具:一个发布文章、一个查询订阅者列表,由 Agent 自行决定要不要调用第二步。这样每个工具都简单、可复用、容易测试,模型也更容易理解工具边界。
在权限控制上,我倾向于在 Host 侧做“用户级授权”,在 Server 侧做“工具级隔离”。比如同一个工具有时会被不同用户调用,Host 负责检查当前用户对该工具是否有权限,Server 则保证不同用户之间数据隔离。这样既灵活,又不会把权限逻辑写死在工具实现里。
3.3 工具描述与 JSON Schema:模型的“API 说明书”不能含糊
MCP 的工具描述,本质上就是一份让模型理解“这个工具是什么、参数怎么填”的说明书。如果工具描述写得含糊,模型在调用时就会出现参数拼接错误、强行调用不存在的字段、忽略必填项等问题。我总结了几条很实用的描述原则。
第一,工具名称要具备“动作+对象”的结构,比如“查询订单状态”和“提交退款申请”,不要用太抽象的“order_process_1”这类命名。模型对语义化名称的理解能力,比对无意义编号强很多。第二,描述字段要写清楚这个工具的适用范围和边界,比如“此工具只能处理付款状态为已完成的订单,其他状态请调用另一个工具”。这个约束条件看似多余,但有效降低了模型的自由发挥空间。第三,JSON Schema 中的每个参数都必须写明描述,尤其是枚举值。我曾见过有工具参数里枚举值写的是数字 1、2、3,但没人告诉模型 1 代表什么、2 代表什么,模型只能瞎猜。后来我在 Schema 里给每个值都加了 enumDescriptions,模型调用准确率立刻提升了。
下面是一个我常用的工具描述示例,形式上跟 MCP 的 tools/call 参数一致。它定义了一个“查询合规文件”的工具,参数里有两个枚举字段,并且明确说明了一个边界条件:只支持已归档状态的文件。
{ "name": "query_compliance_file", "description": "根据文件编号查询已归档的合规文件内容。仅用于状态为已归档的文件,在途文件请调用 query_pending_review_file 工具。", "inputSchema": { "type": "object", "properties": { "file_id": { "type": "string", "description": "合规文件的唯一编号,格式为 CF-YYYYMMDD-序列号" }, "output_level": { "type": "string", "enum": ["summary", "detail", "raw"], "description": "查询结果的详细程度,summary 返回概要,detail 返回核心字段,raw 返回文件全文" } }, "required": ["file_id", "output_level"] } }这只是一个例子,但它说明了一个核心观点:工具描述写得越符合模型的理解习惯,Agent 的编排就会越省心。不要把工具描述当成开发文档来写,而要当成“给模型讲清楚怎么做”的说明书来写。
3.4 多版本兼容与渐进发布:别让协议成为新的“紧耦合”
工具一旦被多个 Host 复用,就会遇到多版本兼容问题。MCP 的好处是协议标准化,但坏处是同一个 Server 可能在跑不同版本的接口定义:有的 Host 还依赖旧字段,有的 Host 已经用上了新能力。如果不处理版本兼容,协议标准反而会变成一种新的紧耦合。
我给团队定的规矩是:每个 MCP Server 都要在注册信息里带上版本号,并在 tools/list 的响应里声明本次会话支持的工具版本。新增工具时默认给新工具命名带上能力标识,比如“query_compliance_file_v2”,旧工具保留一段时间并由 Server 返回 Deprecated 标记。Host 侧收到 Deprecated 标记后,可以在日志里记录警告,但不要立刻移除调用路径,给下游留一个过渡期。
还有一个小坑:MCP 的某些传输方式在连接断开后重新连接时,会丢失部分会话上下文。对于涉及长耗时任务的工具,我建议 Host 在发起调用时就传入一个任务回调地址或任务查询方法,而不是等服务端在连接断开后才回调。这个设计被很多人忽略,但它直接影响 Agent 处理慢任务的稳定性。有关 MCP 的具体传输层选型(比如用 stdio、SSE 还是 streamable HTTP),我的经验是:本地工具用 stdio 最省事,跨服务部署用 streamable HTTP 更灵活,但都要做好重连和超时处理。
4. 三者咬合时最容易踩的坑与排查实录
4.1 长任务中多模态输入变形的定位过程
我记得有一次,用户上传了一张发票图片,Agent 需要从中提取金额并调用报销工具。任务很长,用户在上传之后又问了几个问题,等 Agent 真正开始处理报销时,图片内容在上下文里已经经过了多轮压缩,模型拿到的图片预览被缩得很小、文字都看不清了。最后 Agent 报出了一个完全不对的金额,用户一时还没发现,要不是我们后来复查日志,都不知道问题出在哪。
这个案例暴露出的问题,正是多模态、Harness 和模型三者的衔接失误。根因是:上下文管理器在压缩历史消息时,对图片消息执行了“降采样”,但报销工具要求金额信息必须可靠,降采样后的图片根本满足不了这个精度要求。后来我们的方案是:在 Harness 的消息结构里,为需要高保真的多模态内容增加一个“重要度标记”,上下文管理器压缩时跳过这些内容;同时,在识别环节就把结构化结果(比如发票上的金额、日期、编号)单独存入任务级记忆,后续工具调用直接引用结构化结果,不再依赖图片本身。这个调整之后,类似问题基本没有再出现过。
这里我想分享一个经验:多模态 Agent 里,模型直接看图和结构化解析,永远应该分成两条路线。能转成结构化数据的场景,优先转;只有结构化数据也搞不定的场景,才动用 VLM。这样既提高了准确性,也节省了很多 VLM 调用成本。
4.2 MCP 工具超时与 Harness 线程池的冲突
还有一次压测时,我们发现 Agent 的响应速度会突然从正常的两三秒变成几十秒,而且恢复不过来。查了一圈,最后定位到问题发生在工具执行器。因为工具执行器用了固定大小的线程池,而某个 MCP 工具在被并发调用时会导致部分请求阻塞等待外部接口返回,线程池很快就被打满了。后续新的工具调用全部排队,模型的输出也只能卡在“等待工具结果”的状态里,整个 Harness 就像被堵死了一样。
这个问题的本质是:MCP Server 与 Harness 之间的超时策略、并发控制策略没有对齐。如果 Harness 侧认为某个工具的合理超时是 5 秒,但 Server 侧实际一个操作要 10 秒,那 Harness 反复重试就会造成大量线程被占住。我现在会在两个层面做优化:第一,给工具调用设置完善的超时分层,连接超时、读取超时、总体超时分开设置,并在工具执行器里加信号量和熔断器;第二,把 MCP 工具调用放到独立的异步任务池,和主流程的线程池隔离,这样即使某个工具不响应,也不会把主流程的其他任务拖垮。
排查这类问题时,观测模块的价值会体现得很明显。如果没有“每个工具调用耗时百分比”“池内活跃线程数”这些指标,你可能只能盲目地重启服务。我建议在 Harness 的可观测面板里,把工具调用的耗时分布、重试次数、以及 MCP 连接的重建频率都作为核心监控项。发现异常时,先看耗时分布,再定位具体工具和具体超时阶段,会快很多。
4.3 一个“综合症”案例:识别到了但决策太慢
还有一个案例也挺典型:Agent 明明借助多模态识别正确识别了用户上传的图表,但在后续决策时依然花了很长的时间。这个过程看起来“功能正常”,但体验就是慢。后来我逐段加日志才发现,问题根本不在识别模型本身,而在于 Harness 对多模态结果的后续处理链路太冗余。识别结果生成后,被作为一段很长的文本描述放进了上下文,模型在决策时反复重新阅读这段描述;同时,这段内容又触发了上下文压缩,每轮压缩都要消耗额外时间。
这个问题的解法是:在 Harness 里对多模态识别结果做“二次蒸馏”,把识别结果压缩成几行结构化结论,只保留跟当前任务决策强相关的字段。比如识别出一张包含项目排期的图表,模型决策时真正需要的只是“任务的截止日期”和“依赖关系”,至于图表的原始样式、坐标轴刻度,根本没必要留在上下文里。做出这个改动后,同样的任务决策耗时下降了将近一半。
这个案例让我意识到,多模态 Agent 的性能瓶颈经常不是模型本身,而是“数据在链路中的形态”不合适。模型拿到不对的数据,自然要多绕弯路。
4.4 调试与可观测性建议:把这些指标加进面板
最后分享一下我给类似系统加可观测性指标时的最小清单,按优先级排列:
| 指标 | 具体含义 | 建议阈值 |
|---|---|---|
| 模型请求 token 消耗 | 每次请求的输入/输出 token 数 | 持续超过上下文窗口 90% 时告警 |
| 多模态处理耗时 | 图片/音频/文档从进入系统到变成结果的时间 | P95 超过任务总耗时 30% 时关注 |
| 工具调用耗时分布 | 每个工具调用的 P50/P95 耗时 | 单工具 P95 超过 5 秒时排查 |
| MCP 连接重连次数 | Server 连接异常重建频率 | 每分钟超过 3 次告警 |
| 上下文压缩触发频率 | 压缩事件占请求数的比例 | 超过 30% 时考虑优化记忆策略 |
| 工具执行队列深度 | 等待执行的工具任务数量 | 持续大于线程池核心线程数时告警 |
最后再补充一个我个人的体会:Agent 开发里的绝大多数疑难问题,都不是某一层技术搞不定,而是层与层之间的接口设计出了问题。多模态到 Harness 之间的数据形态,Harness 到 MCP 工具之间的约定,哪一个没对齐,都会在复杂的任务中被放大成看起来很玄学的故障。所以我在过方案时,一定会把“某一种新模态进来之后,消息结构、记忆策略、工具描述要跟着怎么变”这件事提前想清楚,而不是先把模型跑通就完事。多花点时间打磨这些边界,后面省下的排查时间,远比想象的多。