第一次做多模态 Agent我最深的感受不是模型有多强而是工程链路有多乱。模型本身能看图、能读文档、能对话但真正把它塞进一个业务系统里需要处理图片压缩、OCR 结果对齐、工具调用参数校验、上下文保留策略、超时重试机制……这些琐碎但绝不能出错的活业界慢慢总结出一个词叫Harness Engineering。我理解它不是什么新框架而是围绕 Agent 本身搭建外部约束系统和增强系统的一整套工程实践。给模型一副大脑很重要但给它一套合身的骨架、神经和肌肉才是让它在真实场景里跑起来的关键。这篇文章不谈空洞的概念把我自己从零搭建一个多模态 Agent Harness 的过程、踩过的坑、调优过的参数全部摊开来讲。适合正在做 Agent 落地、被上下文管理和工具调用折磨过的开发者参考。1. Harness Engineering 到底在解决什么问题1.1 模型能力溢出之后的工程缺口先看一个真实现象开源多模态模型的推理能力已经能完成看图总结图表 检索历史记录 对比数据 给出建议这类复合任务。但如果直接把原始图片丢给模型不加任何处理几轮对话之后上下文占用会非常夸张——一张 2MB 的截图进视觉模型折算成 token 可能吃掉几千甚至上万 token。多轮调用下来上下文窗口很快告急响应延迟也肉眼可见地变慢。我刚接手某跨平台系统时团队最头疼的不是模型答不出来而是模型答得太飘。同样一张截图模型今天能准确提取表格数据明天换了个模型版本输出格式就变了工具调用参数偶尔还会多出个奇怪字段导致下游接口直接 500。这些问题的根源不是模型本身是缺少一层工程约束。Harness 的概念这时候就很清楚了在模型和业务系统之间加一个由开发者完全掌控的中间层。它负责把多模态输入转化成模型容易理解的标准格式负责约束模型必须按约定的 JSON 结构返回负责决定上下文里哪些内容该保留、哪些该丢弃还负责在模型出错时兜底。模型还是那个模型但有了 Harness行为就变得稳定可控了。1.2 多模态链路里的失控、延迟和成本纯文本 Agent 的链路相对简单但多模态场景有三个独特难题。第一个是失控。视觉模型的输出天然带有概率性同一张发票照片光照不同、角度不同模型提取出的金额字段可能不一致。如果没有 Harness 层面的校验和重试机制这种不稳定会直接传导到业务层。我在项目中遇到过识别结果里金额字段出现金额合计这种文本的情况没有过滤器的话这条数据就进数据库了。第二个是延迟叠加。多模态调用的延迟不是一个点是一条链图片上传耗时、预处理耗时、视觉编码耗时、模型推理耗时、工具调用耗时每一环都在累加。实测下来一个带工具调用的多模态 Agent如果链路设计不合理端到端延迟很容易冲到 15 秒以上这个体验在绝大多数业务场景里是没法接受的。第三个是成本。视觉 token 的价格通常是文本 token 的好几倍。同一段对话请求一次模型和请求三次模型成本差距可能在一倍以上。Harness 要做的一个重要工作就是减少无效调用、控制上下文增长让每一分钱都花在刀刃上。1.3 Harness 不是框架是约束系统市面上提到 Agent很多人第一反应是 LangChain、LlamaIndex 这类框架。但 Harness Engineering 强调的是另一件事不要把所有逻辑都塞给模型自由发挥而是要给它划定明确的边界。打个比方模型像一个能力很强的新员工。你给他任务清单他能独立完成很多事但你不可能指望他第一次上手就完全理解公司流程和接口规范。Harness 就是给他配的操作手册、审批流程和异常上报机制——什么时候该调哪个工具、参数必须按什么格式传、超时了怎么办、结果不合格怎么重跑这些都要提前定义好。这个思路带来的直接好处是模型的可替换性变强了。因为 Harness 做了统一抽象底层从模型 A 切到模型 B只要接口兼容业务层几乎不用改。我实际切换过一次视觉模型因为 Harness 层把所有输入都做了标准化切换成本比我预想低很多。2. 拆解一个 Harness 的六大核心模块2.1 路由与规划层决定模型往哪走Agent 的入口最先经过的是路由层。这个模块要回答两个问题这个请求该不该走 Agent 链路该走哪条链路。以我做的某图像处理 Demo 为例用户可能发来一张截图问这个图表里的数据是多少也可能直接问上周的销量趋势怎么样还可能是闲聊。如果所有请求都走多模态 Agent 链路成本和延迟都受不了。我的做法是一套轻量级意图分类器先用一个快速模型判断请求类型纯文本问答走文本链路涉及图片分析走多模态链路涉及工具调用再挂检索。实测下来这个前置路由能过滤掉大约 30% 的无效请求整体成本下降了接近四分之一。规划层则负责把复杂任务拆成步骤。多模态场景里规划特别容易翻车——模型觉得自己能做所有事结果工具调用链特别长任何一环出了偏差整个过程就得重来。我倾向于用短规划 动态调整模型只规划下一步做完再规划再下一步而不是一口气规划完所有步骤。这虽然看起来不够智能但稳定性大幅提升。2.2 多模态输入标准化统一格式才能稳定输出多模态输入的格式问题远比想象中复杂。用户可能传 PNG、JPG、PDF、甚至直接拍照上传分辨率、宽高比、文件大小千差万别。模型对图像的理解受分辨率影响很大小图看不清超大图又浪费 token。我整理了一套输入处理管线图片统一压缩最长边超过 2048 像素就等比缩放PDF 先渲染成图片再按页送入视觉模型避免直接让模型读 PDF表格类数据优先转成 Markdown 或 CSV 文本让模型走文本理解而非视觉理解准确率更高每张图的 token 成本做预算超过阈值先压缩再送。这套标准化之后我发现模型的输出格式稳定了很多。原因不难理解输入越统一模型的输入分布就越接近它的训练分布输出自然更可控。2.3 工具注册与调用解析约束模型的手脚Harness 里最核心的部分是工具调用机制。模型不能直接执行代码或访问数据库它只能申请调用某个工具Harness 负责鉴权、执行、返回值注入。工具描述写得好不好直接决定模型能不能正确调用。我的经验是工具描述要具体到参数级别。比如查询订单这种描述就太宽泛模型不知道该传什么参数改成根据订单号和时间范围查询订单状态订单号格式为 12 位字母数字组合时间范围为 ISO8601 格式模型调用准确率能提高不少。另外工具返回值也要做瘦身。之前遇到过工具返回了 200 行表格数据模型为了理解这张表又把整张表重新看了一遍token 和延迟都暴涨。后来我把返回值做了摘要只保留关键统计信息和前几条明细模型反而处理得更好。这说明给模型太多无关信息不一定有帮助甚至还会干扰它的注意力。2.4 记忆与上下文管理控制模型看得见的范围多模态 Agent 的上下文管理比纯文本要棘手得多。图片 token 占比高历史图片如果一直留在上下文里很快窗口就不够用了。我目前的记忆策略分三层短期记忆当前任务会话内的原始消息保留最近 4-6 轮工作记忆当前任务的关键数据如图表统计结果、工具返回值摘要持续保留到任务结束长期记忆跨会话的摘要信息以文本向量形式存入库中不保留原始图片。这套三层结构的好处是模型始终只看到它当前需要的信息不会被历史图片淹没。对于需要多轮工具调用的任务我还会在做完一轮之后让模型输出当前状态摘要作为下一轮的上下文输入。实测下来上下文占用能降低 40% 左右而且因为干扰信息少了准确率反而有提升。2.5 安全护栏与审计日志防止模型乱来多模态 Agent 一旦接入真实业务安全就不仅是合规问题更是稳定性问题。模型可能因为幻觉输出不存在的订单号也可能因为 prompt 注入被诱导执行危险操作。我的 Harness 里加了以下护栏输出结构校验模型返回 JSON 必须通过 Pydantic 校验字段类型不对就触发重试工具调用白名单模型只能调用预注册的工具不能凭空发明参数敏感信息过滤模型输入输出都过一遍敏感词和正则匹配防止数据泄露全链路审计日志每一次模型请求、工具调用、参数、返回值全部落日志方便事后排查。这些护栏不是摆设。有一次模型在工具调用参数里加了一个源数据里根本不存在的字段如果没做结构校验这条错误数据就会传入下游系统。审计日志在排障时尤其有用很多疑难问题都是靠对比日志里的输入输出差异找到根因的。2.6 异常兜底与重试机制留好退路模型调用一定会失败只是早晚的问题。超时、限流、返回格式错误、下游接口抖动都可能让 Agent 卡住。Harness 的异常处理要覆盖每一层网络层指数退避重试最多 3 次模型层如果返回 JSON 解析失败将错误信息反馈给模型让它重新生成通常一两次就能改对业务层工具调用失败时让模型基于已有信息输出兜底结果并标注置信度整体超时单条请求设置硬超时超时后立即返回错误码不让用户一直转圈。异常兜底的核心思想是让系统在模型不太聪明的时候也不至于完全崩溃。宁可给一个部分正确的结果也比死等强。3. 亲手搭一个多模态 Agent Harness从架构到代码3.1 环境与技术栈选型我用的是 Python 3.11 FastAPI 做服务端模型层通过异步 HTTP 调用开源多模态模型 API工具层用简单函数注册机制实现。选这套组合主要考虑两点FastAPI 的异步特性适合同时处理多个 Agent 请求Python 生态里处理图像、表格的库最全写工具函数几乎不用额外造轮子。目录结构大致这样agent_harness/ ├── main.py # FastAPI 入口 ├── harness/ │ ├── router.py # 路由与意图分类 │ ├── preprocess.py # 多模态输入标准化 │ ├── context.py # 上下文管理 │ ├── tools/ │ │ ├── registry.py # 工具注册中心 │ │ └── biz_tools.py # 具体业务工具 │ ├── validator.py # 输出结构校验 │ └── auditor.py # 审计日志 ├── models/ │ ├── schemas.py # Pydantic 数据模型 │ └── llm_client.py # 模型调用封装 └── config.yaml # 模型、工具、阈值配置3.2 核心代码骨架三轮状态机我实现的 Harness 核心是一个循环状态机每一轮都做四件事规划 → 调用模型 → 校验输出 → 执行工具或返回结果。代码大致长这样class AgentLoop: def __init__(self, context: ContextManager, tools: ToolRegistry): self.context context self.tools tools self.max_rounds 5 async def run(self, user_input: dict) - dict: self.context.add_user_message(user_input) for round_idx in range(self.max_rounds): messages self.context.build_messages() response await self._safe_call_model(messages) parsed self._parse_response(response) if parsed is None: self.try_recover_after_parse_error() continue if parsed.get(finish): return parsed[answer] tool_name parsed.get(tool) tool_args parsed.get(args, {}) self.auditor.log_tool_call(round_idx, tool_name, tool_args) tool_result self.tools.execute(tool_name, tool_args) self.context.add_tool_result(tool_name, tool_result) return {error: max_rounds_exceeded}这段代码的核心不在调用模型本身而在于每一步都有日志、每一步都有校验、每一步都可能重试。状态机的方式让整个 Agent 的行为变得可预测不会再出现模型自由发挥漫游十几轮的情况。3.3 多模态输入预处理实现预处理模块做的事是把用户上传的原始文件变成模型消耗得起的标准输入。我自己的实现关键部分如下def normalize_image(image_bytes: bytes, max_side: int 2048) - dict: img Image.open(BytesIO(image_bytes)) if max(img.size) max_side: ratio max_side / max(img.size) new_size (int(img.width * ratio), int(img.height * ratio)) img img.resize(new_size, Image.LANCZOS) # 统一使用 JPEG 格式控制体积 buffer BytesIO() img.save(buffer, formatJPEG, quality85) encoded base64.b64encode(buffer.getvalue()).decode() # 估算视觉 token 消耗粗略值实际模型各不相同 estimated_tokens len(encoded) // 1200 500 return {data: encoded, estimated_tokens: estimated_tokens}这里有个很实际的问题不同模型的视觉 token 计算方式不一样。有的模型按图片边长计算有的按实际编码长度计算如果不估算很容易在上下文管理模块里错误预算导致窗口溢出。我在实践里给每张图附加一个 estimated_tokens 字段上下文管理模块基于这个字段做预算虽然不精确但足够用。表格类文件我单独处理优先转成文本def extract_table_to_text(file_bytes: bytes, filename: str) - str: # PDF 转文本用 pdfplumber图片表格用 OCR 后对齐 # 最终统一输出成 Markdown 表格文本 return markdown_table_text这样处理后模型对表格数据的理解从看图猜变成了读文本计算准确率有明显提升。3.4 工具注册与调用链设计工具注册我做了个轻量方案用装饰器声明工具描述和参数模式register_tool( namequery_order, description根据订单号查询订单状态订单号为 12 位字母数字组合, args_schema{ order_id: {type: string, pattern: ^[A-Za-z0-9]{12}$}, time_range: {type: string, pattern: ^\\d{4}-\\d{2}-\\d{2}$} } ) def query_order(order_id: str, time_range: str) - dict: # 调用下游接口返回结果摘要 ...模型可以通过工具描述里的参数模式学会正确调用。为了让工具描述更准确我在调试阶段经常做一件事故意给模型一个模糊的请求看它如何选择工具、如何生成参数。发现偏差就修改描述或增加约束反复迭代几轮后调用准确率一般能稳定在 90% 以上。工具执行完的返回值我默认只保留 1000 字以内的摘要。超过这个长度就先做一轮文本摘要再传给模型避免上下文被大段无意义数据撑爆。3.5 关键参数配置与调优记录这一节分享我实测调整下来的参数可以直接参考参数初始值调整后调整原因最大轮数105轮数越多越容易跑偏5 轮内解决绝大多数任务单轮超时60s20s多模态推理慢但超过 20s 通常意味着卡死重试次数13模型偶发输出格式错误重试一两次能恢复温度0.70.2工具调用场景需要确定性温度调低后参数错误明显减少上下文轮数84图片多时历史轮数保留过多窗口和延迟都吃紧工具结果长度完整返回1000字摘要过长结果干扰模型注意力摘要反而更准温度从 0.7 调到 0.2 是我印象比较深的一次调整。原本以为更自由的生成能让模型更智能实际发现工具调用中参数格式错误暴增。调低温度后虽然创造性差了一些但业务场景里我们需要的从来不是创造性是稳定。3.6 成本与延迟的权衡实践多模态 Agent 的成本大头在视觉 token。以实际项目为例一次图片问答原始图片直接送模型单轮消耗约 3500 token图片压缩到最长边 1536 并转为 JPEG 后单轮消耗约 1800 token几乎省了一半。如果再把历史轮数从 8 轮减到 4 轮整体会话成本能降 60% 左右。延迟方面实测的几组数据可以作为参考方案平均端到端延迟原始图片 10 轮上下文16.8s压缩图片 5 轮上下文9.2s压缩图片 4 轮上下文 工具结果摘要6.5s上面基础再加意图路由前置过滤4.8s这几组数据说明延迟优化的空间主要在 Harness 层不在模型层。模型单次推理时间可能只占三分之一剩下的全是上下文膨胀、无效工具输出和重试开销。4. 实战案例多模态工单质检与智能分诊4.1 场景设计与链路规划拿一个完整的业务场景来走一遍某客服平台需要处理用户上传的截图聊天记录、错误弹窗、发票照片自动提取关键信息然后分诊到对应处理流程。链路规划如下用户上传截图和文字描述前置路由判断是否有多模态分析需求图像预处理压缩、校正、必要时 OCR 对齐模型提取关键字段单号、时间、金额、错误码基于提取结果调用工单查询工具比对历史记录输出诊断结论和分诊建议。4.2 实操链路与效果代码层面关键在第四步的字段提取。我用 Pydantic 规定了模型必须返回的结构class TicketExtraction(BaseModel): ticket_id: str | None None # 工单号 error_code: str | None None # 错误码 amount: float | None None # 涉及金额 occurred_at: datetime | None None image_type: Literal[chat, error, invoice, other] raw_hint: str # 原文线索模型返回后我会把 extraction 结果直接展示给下游系统同时在审计日志里记录原始图片 提取字段 原文字段三个要素。这样即使提取错了兜底也能追溯到原始输入是什么、模型为什么这么判。实测准确率方面常见错误弹窗截图字段提取准确率约 92%发票照片的金额和单号准确率约 85%如果发票图片存在倾斜或遮挡准确率会掉到 70% 左右此时 Harness 会引导用户重新拍照而不是硬着头皮分析。4.3 一次完整的调优过程记录印象比较深的是一次调优工单截图里经常出现当前状态已完成这类文本模型偶尔会把它提取到error_code字段里。问题的原因不是模型不认识字而是视觉模型对哪段文本是错误码的理解不够稳定。我做了两步调整在字段描述里加了明确约束error_code 必须是报错弹窗中的错误码通常为字母数字短码不包括状态描述在工具执行环节加了一层后处理校验如果 error_code 里包含中文或长度大于 20则判定为提取失败触发一次针对该字段的重问。调整后准确率从 81% 提升到了 90% 左右。这说明一个问题Harness 的价值不在于让模型更聪明而在于把模型的错误拦截在业务之外。后处理校验虽然看起来不优雅但它比单纯依赖模型能力更可靠。5. 常见问题与避坑实录5.1 高频问题速查表问题现象根因解决方案模型返回 JSON 解析失败偶尔出现多层嵌套引号或尾逗号大模型输出格式不稳定解析失败后把错误反馈给模型重试同时用正则做预处理工具调用参数带幻觉字段参数里出现源数据没有的字段模型对工具 schema 理解偏差加强描述约束 后端做严格参数校验非法字段直接丢弃上下文窗口不足多轮图片问答后报错或丢失信息历史图片 token 累积历史图片只保留摘要不保留原图响应延迟高端到端超过 10 秒上下文膨胀 工具输出过大压缩图片、限制轮数、工具结果摘要模型动作反复同一问题换不同顺序调用工具温度过高导致随机性温度调整为 0.2 左右图片理解不准小号文字识别错误率高图片分辨率不足预处理时放大局部区域或引导用户重新拍摄5.2 几个值得特别注意的坑第一个坑是上下文轮数不能只看轮数还要看每轮的 token 消耗。纯文本场景 8 轮没什么问题多模态场景 8 轮可能已经吃掉了 80% 的窗口。我最后改为按 token 预算来控制上下文而不是按轮数。第二个坑是模型对工具描述里的免责说明非常敏感。我曾在工具描述里写如果参数不合法请返回错误结果模型反而频繁返回错误而不调用工具。后来把描述改得简短直接只说明必须做什么不说明不允许做什么调用成功率反而上来了。第三个坑是重试有边际递减效应。同一个请求重试超过 3 次后成功概率提升就不明显了。与其死磕重试不如让 Harness 走降级路径比如先返回部分结果再异步补全。我见过有人把重试次数设到 10 次最后把下游系统拖垮了。第四个坑是审计日志的格式和存储要在一开始就设计好。模型调用的输入输出可能很大存原始日志会迅速膨胀。我采用原始数据存对象存储 数据库存索引的方式查日志时先查索引再去拉原始数据既保证了可追溯性又控制了存储成本。5.3 从传统后端工程转过来的人最容易犯的错做传统后端的人包括我自己最容易犯的一个错是把模型当数据库或普通 API 来用追求 100% 确定性和零容错。但 Agent 的本质是概率系统Harness 的目标不是消灭概率性而是管理和兜底概率性。所以代码里应该处处为模型可能出错做设计结构校验、重试、兜底结果、可观测性一个都不能少。另一个容易犯的错是工具设计得过于面向数据库而不是面向模型。工具应该是模型能自然理解的动作而不是后端接口的照搬。把根据订单号查询订单详情改写成查询订单状态输入订单号输出状态和相关时间模型用起来的成功率差别非常大。6. 聊聊 Harness 接下来会怎么走我自己做下来最大的感受是Harness Engineering 会成为多模态 Agent 落地的一项基本功就像当年后端工程师必须懂缓存和消息队列一样。不出意外的话以下几个方向会越来越成熟评估优先于开发。现在大家还在调模型碰运气但成熟的 Harness 会内置评测集每次修改都自动跑回归确保模型能力升级或 prompt 调整后效果不会回退。我自己的项目里已经加了简单的回归测试虽然还比较粗糙但已经帮我挡掉过几次改好了 A 模块却弄坏了 B 模块的问题。标准化协议会慢慢出现。工具描述、上下文格式、审计日志如果各家都不一样生态就长不大。会有一些事实标准慢慢沉淀下来让 Agent 之间的工具互调、模型替换变得更加容易。还有一个我很看好的方向是Harness 的自动化。现在搭建 Harness 全靠手工未来应该会有工具根据任务描述自动生成工具 schema、自动设计上下文策略、自动调参数。就像当年 REST API 从手工写文档发展到自动生成文档一样。最后一句话纯属个人心得别让模型承担它不该承担的责任也别让代码忽略它必须承担的责任。模型的智能是放大器Harness 才是稳定器。多模态 Agent 能不能从 Demo 走向生产很多时候不取决于模型多聪明而取决于 Harness 有没有把后路兜住。
阅读完成 · 觉得有帮助?