最近在做一个内部 Agent 项目,正好把多模态、Harness 和 MCP 三条线全串起来了。我原本习惯的是"纯文本输入 + 模型直接调函数"的轻量玩法,结果任务一复杂,立刻发现这套思路在真实场景里根本撑不住:模型要看截图、要访问外部数据、还要按顺序执行好几轮工具调用,每一步都容易断,断了我都不知道断在哪。
所以这篇笔记,基本就是我这段实践的完整复盘。内容不会铺开讲理论,而是从"这玩意儿到底解决什么问题"出发,把我在多模态、Harness、MCP 上的理解、选型思路、踩过的坑、最后怎么串成一条稳定链路的过程都写出来。适合正在做 Agent 开发、或者已经从"调接口"往"做产品"方向走的人看。
1. 为什么 Agent 绕不开多模态:从"能聊天"到"能看图干活"
1.1 多模态在 Agent 里的定位:不是识别能力,而是情境感知
最开始我对多模态的判断很片面,觉得这就是"模型多了一个看图的能力",差不多等于 OCR 增强版。真上手之后才发现完全不是这个逻辑。
纯文本 Agent 能做什么?它能读你喂给它的文字、能处理结构化数据、能生成内容。但一旦任务变成"你看一下这张报表截图里哪个区域异常,然后去数据库里查一下原因",问题就来了——报表截图里的颜色、布局、趋势线、表格结构,这些信息用文字根本没办法完整描述。即便我人工把关键内容转写成文本,也已经丢掉了大量上下文。
多模态在这里真正解决的不是"识别出图片里写了什么字",而是让 Agent 具备了情境感知能力。所谓情境感知,就是它能直接观察运行环境中那些没法被文字化、结构化的视觉信号,比如 UI 界面当前处于什么状态、数据图上哪个拐点明显异常、一份扫描文档里表格和批注是怎么排布的。
我给一个不太接触这块的朋友打过一个比方:纯文本 Agent 像一个只能通过电话听汇报的调度员,你说什么他听什么,但没法亲眼看现场;多模态 Agent 等于给这个调度员配了监控屏,他可以直接看到机器上哪个灯在闪、哪条流水线堵了。看清楚现场再做判断,跟靠别人转述再做判断,决策质量完全不一样。
不过在真实开发里,千万别把"多模态"当成无所不能的视觉识别器。模型确实能看,但不是所有图它都能看明白,这就要聊到接入方式上的细节。
1.2 图像接入的实操:base64、URL 与 token 大小控制
现在主流的大模型 API 基本都支持两类图像输入:直接传 base64 编码的图像数据,或者传一个可访问的图像 URL。两种方式各有适用场景。
我一开始图省事,统一走 base64。工具函数从截图接口拿到 PNG 之后直接编码塞进请求。好处是链路短、不依赖外部网络,坏处是如果图像非常大,一次请求的 token 消耗会涨得很快。
后来我才认真去算了一笔账。视觉模型的 token 不是按照"图片几个字节"算的,而是把图片切成若干视觉区块(patch),每个区块都要消耗 token。同样是 1920x1080 的截图,有些模型一次要吃上千甚至几千 token,这还不算后续对话轮次里这些图像 token 仍然占着上下文。
这里有个非常容易被忽略的坑:图像 token 会一直保留在上下文里。也就是说你第一轮让模型看图,它记住了,但等到第五轮工具调用结束、Agent 准备生成最终结论时,那张大图依然占着上下文窗口的大块空间。如果中途还得插入外部数据库返回的大量文本,上下文一下子就紧张了。
所以我后来总结出一套处理原则:能压缩就压缩,能不全程保留就不全程保留。具体做法是:
- 先判断原始图像是否过大,超过一定边长的先等比缩放,保证最大边不超过 1280 像素。
- 如果一张图里有精度要求很高的局部区域(比如表格数字),我不直接让模型硬看全图,而是用"两阶段识别":先用低分辨率缩略图让模型定位重点区域,再裁出这部分做高分辨率识别。跟用地图先看全局、再放大地图看某条街是一个思路。
- 进入工具调用阶段之后,给图像消息设置一个生命周期,后续轮次不再重复携带原图,而是携带第一轮视觉模型输出的结构化摘要。
表格里我列一下几种图像输入方式的对比,方便参考:
| 输入方式 | 适合场景 | 主要问题 | 我的建议 |
|---|---|---|---|
| base64 内嵌 | 工具脚本、离线环境、不依赖外链 | 大图 token 消耗高 | 先压缩再编码,控制在合理分辨率 |
| 图像 URL | 已有对象存储、允许外部访问 | 需要保证可访问性和访问权限 | 适合 webhook 流程,注意过期链接 |
| 本地路径 | 本地 demo、调试 | 不走 API 就拿不到图像内容 | 仅限开发调试用 |
还有一个容易忽略的小细节:传 URL 给某些模型时,如果 URL 需要鉴权,模型端是带不了你的 Token 的,访问直接 401。所以别在有权限控制的内部图片链接上走 URL 方案,老老实实做一次图像下载转 base64。
1.3 视觉幻觉与"伪精确"问题
多模态最坑的不是"看不懂",而是"看错了还特别自信"。我在一次测试里,让模型从一张表格截图中提取数据,有一行明明是利润率 3.2%,模型信心满满地输出 32%。单独看输出,格式很漂亮,数据也对齐了,完全看不出破绽。这就是所谓的"伪精确"——输出形态很精确,但内容本身是幻觉。
这个教训给了我两个启发。
第一,凡是模型要从图像里提取数值、关键字段的场景,我不能只让它输出结果,必须让它给出"来源证据"。具体做法是在提示词里要求模型在提取每个关键数值时返回它读取的屏幕区域描述,比如"第 3 行第 2 列"或者"右上角趋势图最后一个数据点"。这样下游代码可以做一个粗糙的交叉验证,人也方便复核。
第二,给每个提取结果附带一个置信度字段。我对模型的要求是:如果你对某个数值有把握,confidence 可以标 high;如果你觉得画面模糊、存在遮挡或者光线影响,必须标 low,不允许硬编。下游收到 low 的字段,就不直接进执行链路,而是进入人工确认队列。
我记得某位前辈说过一句话:让模型承认自己不知道,比让模型永远正确更重要。多模态场景尤其如此,因为图像的歧义空间比纯文本大得多。图像里的反光、遮挡、重叠、模糊,这些人类能自动忽略的干扰,模型全都会当真,所以视觉输出的可靠性必须靠"机制"兜底,而不是靠"模型发挥"。
2. Harness:Agent 不是模型,是模型外面的那套循环
2.1 为什么我放弃了"模型调用工具函数"的直写方式
早期做工具调用,我的代码结构非常朴素:调模型、拿返回结果、如果里面带工具调用指令就解析、执行对应函数、把结果拼回上下文、再调模型。几轮循环写完,小功能跑得挺顺。但一旦工具数量到五六个、一次任务要执行三四轮工具调用,代码就变成一团乱麻。
问题出在哪?出在"模型调用"和"Agent 执行"混在一起了。我把模型当成了整个流程的调度器本身,但模型只是一个生成引擎,它有输出不稳定、上下文窗口有限、偶尔会重复调用工具等毛病。真正该稳定下来的,是引擎外面那套控制系统。
打个比方,模型就像一台发动机,你不能指望发动机自己去规划路线、处理堵车、看导航。你得造一台整车,发动机只管输出动力,方向盘、刹车、仪表盘、变速箱各司其职。这里的"整车",就是我理解中的 Harness。
所谓 Harness,简单说就是 Agent 的执行框架或者编排层。它负责管住这个循环:怎么组织上下文、怎么把模型输出解析成可执行的动作、怎么调用工具、工具结果怎么回注、循环什么时候终止、出错了怎么重试。这些东西如果让业务代码裸写,每个项目重复一遍,而且每个项目都埋着不一样的雷。
2.2 Harness 的最小闭环:记忆、循环、工具注册
我实现的最小闭环,核心就这么几个组件:模型客户端、工具注册表、上下文管理器、以及一个循环驱动器。
工具注册表是第一步。我要求所有工具都注册成统一的格式:工具名、一句话描述、输入参数 schema、执行函数。注意,描述不能随便写。模型看到的是描述,描述得越清楚,模型就越知道这个工具什么时候该用、参数怎么填。我见过太多人注册工具时描述写得很模糊,比如"查询数据的工具",结果模型根本不知道它返回什么格式、参数要求是什么、适合什么场景,直接用错。
循环做起来大概是这样的伪代码思路:
class AgentLoop: def __init__(self, model_client, tool_registry, context_manager): self.model = model_client self.tools = tool_registry self.context = context_manager def run(self, task): self.context.add_user_message(task) for _ in range(self.max_iterations): response = self.model.chat( messages=self.context.messages, tools=self.tools.schemas() ) if response.has_tool_calls(): for call in response.tool_calls: result = self.tools.execute(call) self.context.add_tool_result(call, result) continue if response.has_final_answer(): return response.final_answer raise RuntimeError("max iterations exceeded")这里我特别想强调两个细节。
第一个细节是上下文管理器。工具调用结果、用户提问、中间模型回复都要写进上下文,但上下文总量有限。我的策略是给每条消息标注优先级,低优先级的历史消息在窗口紧张时先做摘要压缩,而不是粗暴地丢弃用户任务本身。这个策略帮我在长任务里保住了主线索,后面会细说。
第二个细节是最大迭代次数。如果一个 Agent 循环没有上限,模型可能因为某些原因永远在做工具调用,既耗时间又耗 token。我在工程上强制要求每次任务必须有一个最大迭代上限,超过就直接终止并报错。这就像心情再好的司机也得有个到达时限,不能让他开去天涯海角。
2.3 我踩过的 Harness 设计问题:超时、重试、死循环
说几个真实踩过的坑,都是常规文档里不会写的。
第一个是工具超时没有兜底。我有一次接了一个外部查询服务,正常情况下响应几十毫秒,但偶尔会卡住。结果模型发起了工具调用,我等了整整两分钟没返回,上下文还停留在"工具调用中"状态。后来我在工具执行时统一加了超时参数,超时返回一个结构化错误,告诉模型"这个工具暂时不可用,你可以换一种方式处理"。加了这层,无谓的等待立刻消失。
第二个是模型不稳定导致的重复调用。有一段时间我发现日志里同一个工具被连续调用了三四次,模型像是卡住了似的,每次拿到结果又重新发起同样的调用,白白烧了一堆 token。排查之后发现,是因为我没有在工具结果里附带上"这次调用对应的 request id",模型拿到结果分不清它是哪次请求返回的,不得不再次确认。把 request id 对应关系理顺之后,重复调用明显少了。
第三个是最危险的死循环。有一次 Agent 在执行一个多步骤任务,它每一步都调工具,但每一步调完结果都没法让它决定下一步,于是它又回去调同一个工具。想象一下,如果我没有设置最大迭代次数,这个循环会一直跑下去,整个任务既不出结果也不报错。现在我在 Harness 里做了两层防护:全局最大迭代次数,加上"连续 N 次调用同一个工具返回相同结果就强制终止"的守卫条件。
这三个坑其实指向同一个结论:Agent 的稳定性不是靠模型调教出来的,是靠 Harness 这层控制逻辑保出来的。模型负责聪明,Harness 负责靠谱。
2.4 结构化输出的脆断:JSON 解析与 schema 校验
跟着工具调用配套的还有一个老问题:模型返回的结构化输出不稳定。即便我在系统提示词里把输出格式写得清清楚楚,模型偶尔还是会返回带多余文本的 JSON、字段名大小写不一致、或者数组里混入意外字段。
我不再直接用json.loads去解析模型输出,而是先做一次"提取 + 修复"。具体来说:
- 用正则或者启发式方法从模型回复中把 JSON 代码块截取出来;
- 解析失败时,尝试做轻量修复,比如去掉末尾多余逗号;
- 成功解析后,必须再用 JSON Schema 约束做校验,字段缺了、类型错了,直接标记为一次输出异常。
有一次发现即使做了 schema 校验,模型还是经常把一个布尔字段传成字符串。后来我用了一个笨办法:在提示词里同时给出正例和反例,明确告诉它"success 字段只允许 true 或 false,不要用 yes/no/1/0"。好模型吃这套,坏模型也能靠校验挡住。
这里我想多聊一句:当你的工具数量开始变多、多个项目都要接数据库、文件系统和浏览器能力时,Harness 里手写的那套工具注册表就会越来越像一个"自定义协议"。而一旦你发现自己在重复实现协议,行业内其实已经有人把这件事做成标准了——这就是 MCP 出现的背景。
3. MCP:把工具接入从"写死"变成"协议化"
3.1 MCP 解决的集成困局
我最早接触 MCP 的时候,心里是有点抵触的。毕竟它本质上就是把"函数调用"包了一层协议,抽象多一层、维护成本多一层、性能还多一分损耗。当时很多项目的工具就那么几个,直接用 function calling 就够了,为什么要上协议?
后来是被实际场景教育了。做 Agent 最头疼的事情之一,就是环境接入:你的 Agent 可能要查询数据库、读文件、操作浏览器、调用内部服务、访问外部 API。每接一个新的工具源,你都要为它写一套客户端逻辑,处理鉴权、错误码、数据格式。更麻烦的是,这些接入逻辑不是一次性的——你下一个项目还要接同样的数据库、同样的浏览器,一切都要重来一遍。这是标准的 N×M 集成问题,N 个 Agent 应用 × M 个能力源。
MCP 的思路就是把这个集成方式协议化。能力提供方实现一个 MCP Server,把自己的工具、数据、交互模板按照统一协议暴露出来;Agent 这边只需要实现一个通用的 MCP Client,就能发现并调用任意 Server 提供的工具。这样一来,N×M 的问题就塌缩成 N 个客户端 + M 个服务端,大家共用同一套协议语言。
就好比电器不用每家厂商各配一套插座,你把插头做成国标,所有插座都长一样,什么电器都能插。MCP 做的就是"插头和插座的标准尺寸"。
3.2 工作机制:JSON-RPC 与三种原语
MCP 基于 JSON-RPC 2.0,消息格式是统一的 JSON。它的工作过程大概分三个阶段:初始化握手、能力发现、按需调用。
初始化的时候,客户端和服务端互相打招呼,确认协议版本、能力集合。握手成功之后,客户端会调用类似"列出工具"的接口,拿到服务端公开的工具清单,包括每个工具的描述、参数 schema。这时候 Harness 就可以把工具清单动态并入模型可用的工具列表,模型知道有哪些工具可以用、每个工具的入参长什么样。到了实际调用阶段,客户端发一个"调用工具"请求,带上工具名和参数,服务端执行后返回结构化结果。整个过程清清楚楚,没有魔法。
MCP 定义了三种原语,它们解决的问题不一样,我整理成了表格:
| 原语 | 作用 | 类比 | 我的使用场景 |
|---|---|---|---|
| Tool(工具) | 发起一次可执行操作 | 一个可以点的按钮 | 查询数据库、执行脚本、发送请求 |
| Resource(资源) | 提供可供读取的数据内容 | 一个可翻阅的资料架 | 读取配置文件、读取模板文档 |
| Prompt(提示词模板) | 提供可复用的提示词片段 | 一句常用话术卡片 | 标准化系统提示词、常见任务模板 |
我最初有一个误解,以为 MCP 是把模型装进服务端,让模型通过协议去调用其他东西。实际不是这样:模型本来就是客户端那端的"大脑",MCP 只是帮这个大脑把"手"伸出去的标准通道。大脑负责决定干什么,MCP 负责把手伸得够远、够规范。
3.3 客户端接入细节:SDK 薄封装、流与错误处理
很多人觉得用了官方 SDK 就万事大吉,但真实项目的坑往往出在底层细节上。
首先是握手失败。MCP 客户端在连接服务端时需要初始化握手,如果服务端和客户端的协议版本不匹配,连接会直接失败。我遇到过一次,服务端更新后客户端还在用旧版依赖,报错信息也不太友好,排查了半天才发现是版本对齐问题。现在我的做法是对所有 MCP 连接做统一封装,初始化失败时主动降级重试,并且把版本不一致的情况单独拎出来打日志。
其次是工具返回内容过大的问题。有些 MCP Server 的查询工具会一次性返回几百行数据,如果这些数据直接进上下文,上下文窗口会迅速告急。我在 Client 侧做了一个拦截层:工具返回先走"摘要优先"策略,上下文里只放摘要和关键统计量,完整数据只在实际需要时才按需加载。这一层看起来简单,但在真实任务里省下的 token 相当可观。
最后是错误分类。我把 MCP 调用错误统一归类成四类:网络不可达、认证失败、超时、业务错误。Harness 会针对不同类型做不同处理:网络不可达重试、认证失败直接报错不继续、超时走降级方案、业务错误反馈给模型让它调整策略。这比我早期"所有异常一律重试"的做法科学得多。
3.4 我保守过一阵子、后来改观的三个原因
我必须承认,MCP 刚出现时我是唱过反调的。但用过一段时间之后,我在三个点上改变了看法。
第一点是生态价值。协议一旦统一,工具接入就不再是一次性的了。我接好了一个浏览器控制工具,它不只能在我的项目里用,换一个 Harness 一样能用。工具变成了可复用的标准件,这是直接写函数调用给不了的。
第二点是描述规范性。MCP 强制要求 Server 提供工具描述和输入 schema,这个"强制"逼着工具提供方把接口文档写清楚,而文档清楚这件事,对模型能不能正确使用工具有决定性影响。我见过太多内部工具没有描述、参数名暧昧不清,模型根本不敢用或者不会用。协议化之后,这种"工具卫生"问题从源头改善了很多。
第三点是扩展空间。MCP 除了工具调用之外还有 Resource 和 Prompt 原语,这就给了 Agent 一种更标准的方式去绑定数据和模板。比如我可以把某个数据源注册成 Resource,模型可以直接读取;把某个复杂的分析流程注册成 Prompt,用户一触发就有了标准操作路径。这套机制跟纯工具调用组合起来,灵活度高出不少。
但我要说一句公道话:如果项目很小、工具就两三个、也没有复用需求,直接用 function calling 确实更轻。MCP 的价值在工具数量多、复用性强、多个系统要协同的场景下才真正显现出来,不必为时髦而硬上。
4. 三件套合流:一条截图任务的完整链路复盘
4.1 一个模拟任务:根据一张业务截图生成处理建议
理论聊得再多,不如跟着一条真实链路走一遍。
我拿一个内部测试任务当例子:输入是一张业务报表截图,截图里有趋势图、表格数据,旁边还有几条手写的批注。任务要求是:理解截图内容,然后去某个数据源里核实一条指标数据,最后输出一份结构化的处理建议。
这个任务看起来简单,但只用纯文本模型做不了——它需要"看图";只靠模型自己完成不了——它需要"查数";而查数这个动作的接入方式就是走 MCP。于是多模态、Harness、MCP 第一次同时派上用场。
我把整条链路拆成了四个节点:多模态解析、Harness 编排、MCP 数据源注入、最终输出与人工校验。下面逐个说。
4.2 节点一:让视觉模型"说出"它在图上看到了什么
这个节点的目标很简单:把一张截图转化为结构化描述。但转化质量的差别可以很大。
我一开始直接问"请描述这张图片",结果模型给了一段比较开放的自然语言描述,后续根本没法程序化使用。后来我重新设计了系统提示词,把输出格式钉死成三段式:先描述图像整体结构,再按区域提取关键数据,最后组织成带来源标记的条目。也就是说,我不要一段"描述",我要一份"数据提取结果"。
提示词的核心部分大概长这样:
请按以下步骤处理用户提供的图像: 1. 先输出"图像结构概述",说明图中包含哪些主要区域; 2. 再按"区域提取",对每个关键区域输出: - region: 区域名称 - fields: 该区域内提取到的字段列表 - confidence: high/medium/low 3. 如果某个数值无法确认,confidence 必须标 low,不得猜测。实际跑下来,结构化的程度明显提高,下游解析也顺了。但前面说过的"伪精确"问题还是出现了一次:模型把一个表格里的 3.2% 读成了 32%。好在这一次我设计了置信度校验和来源标记,低置信度字段进了复核队列,没有直接污染后续流程。这个节点给整个链路的第一条经验是:多模态输出永远只当"草稿状态",必须有校验机制兜底。
4.3 节点二:Harness 里的一次多工具决策
图像解析完成之后,Harness 接手编排。模型拿到结构化图像摘要,发现趋势图里有个区域异常陡增,这时候需要去找外部数据核实。于是模型发起了工具调用,走的正是 MCP 暴露出来的一个数据查询工具。
在 Harness 的视角里,整个过程是这样的:
- 第一轮,模型收到系统提示词、图像摘要、任务描述。
- 模型判断需要查数,发起语义化的工具调用请求,参数是"指标名称 + 时间范围"。
- Harness 校验参数 schema,然后通过 MCP Client 执行数据查询。
- 数据返回后,Harness 做了摘要化处理再写进上下文,而不是把几千行原始数据全塞进去。
- 模型读到了核实数据,确认截图里标注的异常在数据库记录里确实存在,继续下一步。
这里有一个细节特别重要:工具返回的数据必须结构化回注。我把查询结果规范成{指标, 数值, 时间, 趋势说明}这样的格式,而不是让模型看到一张乱七八糟的数据表格。结构化回注能显著降低模型在后续推理时的理解成本,这也是 Harness 可以在工具执行后做的事情。
另外,模型发起工具调用的时机,本质上是由 Harness 里的"规则 + 模型决策"共同定的。我在系统提示词里加了这样一句,效果很好:"如果你发现截图里的数据与常识或经验存在可疑差异,调用查询工具核实后再作答。"这条规则让模型在遇到异常数据时主动去查外部数据,而不是信口解释。
4.4 节点三:外部数据回注与最终生成
等查询结果回到上下文,模型手里就同时有了两个信息源:多模态解析出来的截图数据,加上 MCP 查询回来的数据库记录。
最终生成阶段,我要求模型把所有数据条目都标注来源。比如最终报告里出现"指标在 3 月出现异常增长"时,旁边要注明"依据:截图趋势图区域(来源置信度 medium)+ 数据库记录(时间范围 2024 年 3 月)"。
这个"引用来源"的要求,看起来多此一举,实际上带来两个好处。第一,人工复核的时候,能快速判断这条结论是靠哪个信息源支撑的,截图来源和数据库来源的可信度不一样,覆盖了"模型看过一张图就胡说"的风险。第二,当两个信息源冲突时,模型会被迫在输出中暴露这个冲突,而这个冲突本身就是一个值得关注的信号。
阶段性的结论是:多模态给了 Agent 感知能力,MCP 给了它外部数据的获取能力,而 Harness 负责把这两股信息流编排到一起。三者各自解决一块问题,缺了哪个环节,整条链路都运转不起来。
4.5 校验与人机协作:我不再要求它一次做到 100%
在跑了大量任务之后,我给自己定了一个原则——永远不要把多模态 + 工具调用的链路当成"全自动高可靠"系统,而是当成"多节点人工复核"系统。这听上去像是在退缩,实际上是更务实的定位。
具体做法是在最终输出后面加一个复核钩子。规则校验先跑一遍:数值是否在正常范围、日期是否合法、字段是否缺失。校验不过的就标红,进入人工快速确认队列。人不用从头到尾看一遍 Agent 的工作,只需要看那些"模型自己都不确定"或者"规则判断异常"的地方,几分钟就能审完。
这时候我才真正体会到什么叫"链路稳定性"。整条链路的可靠性不是看某个模型的准确率高不高,而是看每个节点的失败模式是不是可控:图像解析失败可以重试、工具超时可以降级、置信度低可以进人工,每一环都有退路,整体就不会崩。
5. 反模式与稳定性经验:跑了上百次任务之后的几条结论
5.1 反模式一:无限堆上下文
我见过不少 Agent 项目的做法,为了让模型记住更多历史信息,把所有消息都堆在上下文里,结果 token 消耗爆炸、响应越来越慢。这不是"记住更多",这是"记了一堆垃圾还舍不得扔"。
我现在的上下文管理策略很简单:每条消息带重要等级标签。用户任务本身永远最高级,工具返回的中间结果次之,模型早期产生的冗余思考最低。当上下文逼近阈值,低等级内容先被摘要化甚至移除。这个方法最大的好处是任务主线索永远不会丢,而拖慢速度的噪音会被系统性铲除。
5.2 反模式二:把工具结果原始拼接
工具调用返回的原始 JSON 可能又臭又长,几十个字段里模型真正关心的只有两三个。如果把这些原始内容一股脑拼进上下文,等于是让模型在一堆噪音里找信号,既影响推理质量,又浪费 token。
我的做法是:每个工具注册的时候,同时定义好回注策略,默认只输出 summary 字段和关键统计量,完整原始数据存到外部缓冲区。模型在绝大多数情况下只需要看摘要就能决策,只有在明确需要深挖时才通过另一个工具读取原始全文。这个"摘要优先 + 按需加载"的模式,是我的 token 治理体系里最有效的一招。
5.3 反模式三:对工具错误不分类
早期我做工具调用,错误处理就一层try/except,一出错就重试,重试还失败就退出。后来发现这个做法特别蠢,因为它把"网络抖动导致的重试型错误"和"参数传错导致的永久失败"混在一起了。网络抖动重试十次可能成功,参数错误重试一百次也不会成功,只是在浪费时间。
现在每个工具 handler 返回的错误都是结构化对象,包含 error_code、message、retryable 三个字段。Harness 拿到错误以后,只有 retryable=true 才进入重试逻辑;否则直接反馈给模型,让模型考虑换一种执行方案。这个改动很小,但让 Agent 面对异常时的表现从"反复撞墙"变成了"聪明换路"。
5.4 最后几条个人经验
跑了几百次任务之后,我有一点很深的感受:做 Agent 开发,真正难的不是让模型变聪明,而是让整个链路变得可靠。模型输出永远是概率性的,但 Harness 和 MCP 这套框架可以把不确定性封装在可控的边界内,让每一次失败都有迹可循、有法可解。
如果让我给后来者提几条最实在的建议,大概是这些:
- 多模态输入永远先压缩再进上下文,记得给提取结果留来源标记和置信度;
- Harness 要把最大迭代次数、死循环守卫、结构化错误处理当成标配,而不是可选功能;
- MCP 先只上 Tool 原语,跑通后再探索 Resource 和 Prompt,不要一上来就把协议层做得很重;
- 工具结果一律摘要化回注,原始数据按需加载,这是 token 治理的核心;
- 最后,给整条链路保留人工复核节点。多模态 + 外部工具并不能消灭错误,但它能把错误收敛到"可控且可审"的范围里——做到这一步,就已经胜过绝大多数直接裸调的方案。
这篇笔记写到这里,我自己的下一步计划是在 MCP 的 Resource 侧继续做文章,把更多的数据源以标准资源的方式暴露给 Agent。说实话,这个方向挺值得期待,等有了新进展再继续分享。