上周我在帮一个团队排查多模态客服Demo的质检误判问题时,遇到一件很典型的事:提示词已经被他们改得相当精细,系统指令、输出格式、边界示例样样齐全,可模型还是把“屏幕轻微漏光”识别成“外壳破损”,一整天都在给用户推送错误的换货建议。查到最后,问题根本不在提示词,而在模型看到的整个上下文——一段七轮对话历史里出现了四次“破损”“换货”,图片原图带着周边杂物一起进推理,知识库片段整段复制粘贴了好几屏。提示工程把“话”写好了,上下文工程却把“噪音”源源不断送进了窗口。
这件事让我重新梳理了多模态场景下的优化思路。标题里提到的“提示工程架构师的上下文工程优化”,本质上就是一件事:在提示词之外,系统性管理进入模型上下文窗口的全部信息——压缩掉低价值内容、筛选出高相关性内容、再以模型容易定位的结构去组装。这篇就围绕这个核心展开,聊聊我实际踩过和填平的坑,也给出可以直接复用的步骤、测评方法和避坑清单。适合正在做多模态问答、多模态质检、图文客服等应用的开发者参考,不管你是自己调API还是带队做Agent,这套思路都能用上。
1. 为什么提示词写对了,输出还是不对:从“提示工程”转向“上下文工程”
先把这个转变讲透。提示工程解决的是“怎么把意图说清楚”,它默认下游模型拿到的是干净、精炼的输入。可真实业务里,模型收到的往往是一个被各种历史记录、图片块、知识片段堆满的“大杂烩”。多模态场景尤其严重,因为一张图可以折成几百上千个视觉token,一段语音可能比一整段文字还占地方。注意力是有限的,窗口里塞太多无关内容,真正重要的信号就会被稀释。
1.1 一次被“很长上下文”坑掉的失败经历
那个电商售后项目,问题分类任务本身很简单:用户上传商品图片和文字描述,模型输出“破损/漏光/色差/其他”四类,外加处理建议。团队把主要精力都花在完善system prompt上——写了1200多字的规则,还配了三个few-shot样例。但线上准确率始终上不去,漏光经常被归成破损。
我让团队把一次完整请求的usage和messages全部打印出来,按层数了一遍token:
- 系统提示:1802 tokens
- 用户文字描述:1243 tokens
- 历史对话:11轮共5230 tokens
- 图片块:两张原图共2570 tokens
- RAG知识片段:3段共3189 tokens
- 合计:超过13000 tokens
其中真正承载“当前用户这次诉求”的,只有用户文字描述和图片里的关键区域,加起来不到3000 tokens。而历史对话里四次出现“破损”“换货”,RAG片段里五处重复“漏光属外观瑕疵”这句话。模型要在13000 token里找重点,注意力天然会向高频词偏移。
后来我们把上下文重新组装,只保留最近一轮原文、之前的历史压缩成摘要、图片做裁剪、知识片段改写为规则卡片,上下文从13000砍到约4100 tokens,漏光和破损的误判直接降了七成。这个案例让我彻底确认了一件事:在多模态场景,上下文工程的杠杆远大于多写几句few-shot。
1.2 上下文不是“背景资料”,而是注意力预算
用一个开会类比:会议室里坐了二十个闲人,真正说话的人声音再大也很难被记住。上下文窗口也是如此,模型注意力需要在全部token中分配,token总量越多,每个有效信息的权重就越低。
我把上下文工程拆成三条主线,后面所有方法都围绕它们展开:
- 压缩:去掉低信息量内容,减少总token数
- 筛选:保留与当前任务高相关的内容,提升信息密度
- 结构化:用标签、顺序、层级告诉模型关键信息在哪
这三条主线分别对应“少占地方”“只进该进的”“让模型好找”。
2. 多模态上下文到底由什么组成:一次完整的上下文盘点与量化
很多开发者聊上下文,只会想到“历史消息”,但在多模态请求里,上下文的构成远不止这些。系统提示、用户文本、历史对话、图片音频块、RAG检索片段、工具返回结果,全部会拼进同一个序列。不把每一层盘清楚,优化就是盲人摸象。
2.1 一张上下文调用里的“隐藏账单”
我把常见的上下文输入层列成一张表,基本能覆盖大多数业务场景:
| 层级 | 典型内容 | 常见Token量 | 可控性 |
|---|---|---|---|
| 系统指令 | 角色设定、任务边界、业务规则、few-shot样例 | 300~3000 | 高 |
| 用户交互层 | 当前问题、图片、语音、多轮历史 | 1000~10000+ | 中高 |
| 多模态载荷 | 图片块、音频片段、视频关键帧 | 每张图几百到上千 | 中 |
| 检索注入层 | RAG切块、FAQ命中结果、知识卡片 | 500~3000 | 高 |
| 工具返回层 | 函数调用结果、API响应、错误信息 | 200~2000 | 中 |
一张512×512的图片,模型会把长边切成若干块,每块再展开成token序列,实际占用可能超过一千。音频片段同理,逐帧采样后展开。所以多模态场景的“上下文预算”比单纯文本场景更紧张。
2.2 先量化再优化:一次多模态请求的Token账本
不量化就没有方向。我每次接入新场景,第一件事是写一段很简单的脚本,把请求里的每一层分别打印token数,再打印调用返回的usage字段。下面是一个通用的统计思路示意:
def count_message_tokens(messages): total = 0 for msg in messages: part_tokens = [] for part in msg.get("parts", [msg.get("content", "")]): # 文本直接用分词器统计 # 图片按比例估算:长边切块数 * 每块展开数 if part.get("type") == "text": tokens = len(part["text"]) // 示例,实际用离线分词库 elif part.get("type") == "image": tokens = estimate_image_tokens(part["size"]) else: tokens = 0 part_tokens.append(tokens) total += sum(part_tokens) print(msg.get("role"), part_tokens) return total估算图片token不需要很精确,能看出量级趋势就行。目的不是抠数字,而是知道哪一层在悄悄吃掉预算。我见过的项目里,历史对话最常见超标,RAG重复内容次之,系统提示反而是比较容易收敛的那一层。
2.3 上下文工程的三条主线:压缩、筛选、结构化
量完账本,接下来就分清手忙脚乱。压缩对应“降总量”,筛选对应“提密度”,结构化对应“降检索成本”。
拿历史对话举例:压缩是把11轮长对话缩成两行摘要;筛选是只保留包含订单号、问题描述、承诺信息的关键轮次;结构化是把摘要放进一个带角色标签和历史锚点的JSON里,让模型一眼知道哪句话是用户说的、哪句话是客服承诺的。三条线同时做,单靠一条的效果都会打折。
3. 五个实战瘦身维度:让上下文预算花在刀刃上
这一节是全文最有操作性的部分。以下五个维度来自我多个项目的实际调优,不涉及特定平台私有细节,全部通用。
3.1 检索注入不能整段粘贴:改成“召回-改写-事实卡”
RAG最经典的错误,是拿到TopK文档后把整段内容原样塞进上下文。检索出来的段落是为“阅读理解”设计的,不是为“当前任务作答”设计的,里面有一堆背景铺垫和重复表述。
我现在的做法是走三步:
- 召回Top5片段,先用规则或小模型过滤掉与当前query语义距离过远的段落;
- 把剩余片段改写成“事实卡”,每张卡一行“主体-属性-值/结论”,两个片段重复的内容只保留一条;
- 按任务需要的顺序排列,时间、金额、型号这类实体信息放在卡片列表前面。
事实卡的结构可以用JSON,模型对这种结构化格式的定位速度远高于长段落。示例:
{ "facts": [ {"subject": "漏光问题", "rule": "单点漏光属外观瑕疵,仅补偿,不换货"}, {"subject": "售后时效", "rule": "签收7天内可退,15天内可换"}, {"subject": "订单状态", "rule": "订单SO-1024X已发货,物流中"} ] }改造完你会发现,同样一批知识,上下文从3000 token缩到500 token,模型抓规则的准确率反而更高。
3.2 多模态载荷瘦身:图像降采样、裁剪、抽帧
多模态输入不能原样进上下文,这是很多团队忽视的点。图像上的无关背景、水印、对话框,全部会转换成视觉token参与注意力计算。
对于图片,我建议按这个顺序处理:
- 先做ROI裁剪:若任务只关注商品缺陷区域,先用一个轻量检测模型把目标区域框出来,只传裁剪结果;
- 再降采样:最长边压到1024或768,保证关键特征可见即可;
- 最后控制数量:多张图片时,优先选最能体现问题的一张主图或按缩放拼版,不要一股脑全传。
音频输入则是“先VAD、后ASR”的思路——先把有效语音段切出来,再转成文字上下文,而不是直接把音频大段喂给模型。视频同理,只抽2到4张关键帧。
需要特别提醒:压缩多模态载荷时要考虑任务类型。纯视觉判断任务(比如颜色深浅、空间位置关系)不适合压成纯文本摘要,会丢失关键信息。这个坑我后面专门讲。
3.3 对话历史分桶:最近轮次原文、摘要桶、关键锚点
对话历史是最容易无意识膨胀的部分。一个用户聊了十轮,每一轮都带长回复,全部原文进上下文,很快就把预算吃光。
我的做法是三桶结构:
- 最近桶:保留最近3~5轮原文,维持上下文连续性;
- 摘要桶:更早轮次滚成摘要,按主题聚合,不做逐条翻译;
- 关键锚点桶:历史里必须保留的原文实体——订单号、地址、型号、客服承诺内容,单独存一份。
摘要也不是自由发挥,要“带槽位”。比如“用户反馈屏幕有亮斑,型号XX,订单SO-1024X,客服已引导重启验证”,这里面型号和订单号就是槽位,必须保留。直接用模型去“概述前五轮聊了什么”,大概率会把实体丢掉。
3.4 角色与时序分离:模型需要的不是全部历史,而是可信事实
上下文里除了“有什么”,还要考虑“谁说的、什么时候说的”。用户描述可能是猜测,客服回复可能是承诺,这两种信息的语义地位完全不同。
我建议在历史摘要和原文标记里,显式加角色标签与时序索引。让模型知道哪些是待确认诉求、哪些是已生效承诺。下面是一个简化示例:
{ "history_current": [ {"turn": 1, "role": "user", "time": "2025-02-01T09:12:00Z", "content": "屏幕有亮斑"}, {"turn": 2, "role": "assistant", "time": "2025-02-01T09:13:00Z", "content": "已引导重启验证"} ], "history_summary": [ {"topic": "订单状态", "anchor": "订单号SO-1024X", "status": "已发货"} ] }时序信息做成相对索引比堆时间词高效得多。模型不需要反复阅读“周一上午用户说……周二下午客服回复……”,它只需要知道哪件事在前、哪件事在后。
3.5 指令精炼与格式复用:少写形容词,多写可执行规则
系统提示是最容易“越写越长”的地方,因为每个人都会忍不住追加“特别注意”“务必仔细”这类话。这些话对模型没有信息量,纯粹占预算。
我的经验是系统提示里禁止写软话,只保留可执行规则。区别很明显:
- 软话版:“请务必仔细检查用户描述的每一个细节,不要遗漏任何关键信息,输出要专业完整”
- 规则版:“输出问题分类;若证据不足,标记unknown;分类字段取值仅限:破损/漏光/色差/其他”
few-shot也尽量控制在两个以内,只保留边界case,因为样例本身也会稀释任务信号。再把整个上下文的组装模板固定下来,同一任务每次都用同一结构,方便命中缓存,这个我在避坑部分展开。
4. 一次真实改造:某多模态客服系统的上下文优化全流程
把前面五个维度串起来,给你看一个完整案例。项目背景是某零售企业的多模态售后质检Demo,输入是用户上传的商品图片和文字描述,输出是“问题分类 + 处理建议”。
4.1 改造前的上下文与问题表现
改造前的情况非常典型:
| 维度 | 改造前 | 现象 |
|---|---|---|
| 系统提示 | 1200字 + 3个few-shot | 规则含混,软话多 |
| 历史对话 | 全部原文带入 | 关键词重复干扰判断 |
| 图片 | 原图直传 | 背景杂物拉低识别率 |
| RAG片段 | 整段复制 | 内容重复、占预算 |
| 输出格式 | 自由文本 | 结果不稳定 |
线上表现为:漏光误判为破损明显偏多,分类召回率约58%,首token平均延迟2.1秒,多轮后模型会“遗忘”前面已确认的事实。
4.2 改造步骤与效果对比
这次改造动了四个环节:
- 系统提示压缩到350字,删掉所有软话,只保留分类定义、证据不足时的兜底规则、输出格式枚举;
- 历史对话改成三桶结构:最近3轮原文、更早轮次摘要、订单号与型号写入锚点字段;
- 图片先做ROI裁剪,再压到最长边1024,多角度图只保留最能体现缺陷的一张;
- RAG的3段FAQ改写为5条事实卡,重复项去重,并固定JSON结构。
组装后的上下文大致长这样:
{ "system": "你是售后质检助手。分类仅限:破损/漏光/色差/其他。证据不足时输出unknown。", "history_current": [ {"role": "user", "content": "屏幕这里有一条亮线"}, {"role": "assistant", "content": "请上传清晰照片确认"} ], "history_summary": [ {"topic": "订单状态", "anchor": "SO-1024X", "status": "已发货"} ], "image_roi": {"length": 1024, "width": 1024, "object": "screen"}, "facts": [ {"subject": "漏光", "rule": "单点漏光属外观瑕疵,仅补偿不换货"} ] }改造后同一批测试集上,分类召回率从58%涨到87%,首token平均延迟从2.1秒降到0.9秒,上下文总长度从13000降到4100左右。关键在于,这个提升不是某一项单点动作带来的,而是把每一层都梳理后协同生效的结果。
补充一个容易犯的错误:如果只压系统提示,其他层不动,大概率看不到明显变化。上下文工程要整体治理,不能只做局部优化。
5. 上下文工程的评测与监控:不止看得分,还要看信息流
优化完不能凭感觉验收,需要一个可持续的评测与监控机制。这个环节我踩过不少坑,分享下现在稳定的做法。
5.1 离线评测:用固定用例量化上下文变化
离线评测的核心是“控制变量”。我通常这样建评测集:每个业务分类准备正例反例各20条以上,再额外加边界case和多轮case。评测时只改上下文组装策略,模型本身不动。
必测指标有三个:
- 端到端任务得分:分类正确率、召回率、边界case通过率
- 上下文有效密度:有效信息token数 / 上下文总token数,这个比值越高,说明内容越精炼
- 噪声敏感性:在输入里故意插入一段无关上下文,看最终结果是否偏移;偏移越大,说明原上下文对噪声的抵抗力越差
每次改动跑三遍取平均,能有效避免单次随机性导致的误判。
5.2 线上监控:记录、回放、告警
上线后要盯三件事:
- 记录:请求入参的上下文摘要(脱敏后),不存原文,避免隐私问题;
- 回放:每周抽样拿线上失败案例重新跑一遍,对比当前策略和baseline,定位是缺信息还是模型能力不足;
- 告警:设两个简单阈值——上下文总长度周环比增长超30%触发告警;上下文有效密度低于0.15重点复盘。
我还会用“只带系统提示 vs 完整上下文”的A/B重跑来做归因:如果只带系统提示的结果和带完整上下文差不多,说明检索注入或历史信息没有起到增益,反而可能在拖后腿。
6. 落地过程中容易翻车的六个细节
最后这部分是我觉得最值钱的,全是实际做项目时踩过的坑,整理成六个清单项,每一条都能省你两三天排查时间。
6.1 压缩与筛选的副作用:信息塌方
压缩不是无损耗的。最典型的问题是摘要丢实体——模型觉得“地址”不重要,就自动省略了,导致后续推荐逻辑拿不到完整收货信息。处理办法是给摘要加实体校验,抽出时间、金额、型号、地址等固定字段单独回填,不让摘要自由发挥。
另一个坑是过滤误伤。多模态场景里,某张看起来“无关”的背景图,可能藏着缺陷证据。所以相关性过滤要按任务类型配置,纯视觉判断任务不能套用文本检索的过滤规则。
6.2 工程链路层面的三处隐患
第一,动态摘要会影响缓存命中率。上下文每次组装都变,服务端就没法缓存这部分前缀,成本和延迟都会上来。我的做法是把系统提示、固定规则、工具定义放在稳定的前缀段,把历史摘要放在尾部可变段,尽可能让可缓存前缀保持稳定。
第二,只优化首轮、不治理多轮累积。很多团队把首轮上下文做得很好,但对话进行十轮后,摘要桶没更新、原文桶越挤越大,上下文又开始膨胀。解决方案是把“摘要回写”做进无状态服务逻辑,每一轮结束后增量更新摘要桶和锚点字段,而不是每次从原始记录重新组装。
第三,没有小流量AB就全量上线。上下文策略改动对效果影响很大,我建议按5%流量先跑两周,对比线上指标后再逐步放量,避免一次性引入未知风险。
我现在的习惯,是每个项目都维护一份“上下文配置图”,图表里标清楚每一层的内容来源、token预算上限、更新机制和负责人。这样无论谁来改动,都知道当前上下文是怎么组装的、动某一层会影响什么。上下文工程不是一次性的调优动作,而是一个持续治理的过程,把这些细节固化成流程,多模态应用的性能才能真正稳定下来。