
AI Agent 的系统提示词一换模型就崩Harness Engineering 里最耗时的排查常常卡在这一步。TaoToken 给的路径很短一把 Key 从 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建之后在 LangChain / LlamaIndex 里切模型要改的只是一个字符串和一处 base_url。GPT-4 上打磨了两个月的系统提示词换到通义千问之后 Agent 开始跳步骤明明要求先检索再回答它张口就编明明约定只返回一段 JSON它给你裹上一层 markdown 围栏本该拒绝越权请求它反而热情地回一句我尽量帮你试试。这时候多数人的第一反应是把提示词推倒重写写完 GPT-4 那边又开始不稳——因为问题不在提示词本身质量而在于它跟某一个模型的行为习惯绑得太死。比较省事的顺序是先把模型这一项从业务代码里抽出来当变量再用同一份系统提示词、同一批固定输入、同一套采样参数跑两次对照。哪一段角色设定、约束条款或推理规则在新模型上失效会自己浮出来。下面按这个顺序走一遍全程不改 Agent 的工具实现只动 LLM 配置和提示词分层。1. 先分清是硬报错还是软失败GPT-4 到通义千问的落差不在聪明程度1.1 三种典型崩法先对照自己中了几条模型切换后 Agent 表现下滑大致落在三类里处理方式完全不同。第一类是角色漂移。系统提示词里写着你是某某领域的资深审核员遇到不确定的信息必须标注来源GPT-4 会把这个身份贯穿整轮对话换成通义千问之后前两轮还像审核员第三轮开始变成通用助手语气变客气、边界变模糊。这类问题的根因通常是角色段写得太文学化靠形容词撑起来的身份换个模型就撑不住了。第二类是约束失效。比如禁止在未确认用户身份前执行写操作不允许编造接口返回字段。GPT-4 对这类否定式约束的执行率很高通义千问可能只在第一轮遵守一旦上下文变长就被后面的任务描述盖过去。这类失效最隐蔽因为单测时往往看不出来要跑多轮才暴露。第三类是结构失守。要求返回严格 JSON结果多了三行解释要求工具调用参数里必须带case_id结果偶尔漏字段。这类问题最容易量化也最容易修加一个校验函数就能统计出失败率。把这三类分开记录比笼统地说新模型不行有用得多。因为角色漂移要改的是角色段写法约束失效要改的是约束的位置和强度结构失守往往只需要在提示词里补一个完整示例。1.2 别急着改提示词先把模型抽成配置项很多人的做法是发现 Qwen 表现差就把提示词改成 Qwen 专供版本然后 GPT-4 那边的效果也跟着掉。这个来回折腾的成本比一开始就把模型做成变量高得多。正确的前置动作只有一个把模型 ID、base_url、API Key 三件事从硬编码里挪出来变成一处可替换的配置。做完这一步A/B 对照才有意义——否则你连这次变化是提示词改的还是模型换的都说不清。后面的章节就按这个顺序先接上统一入口拿到 Key再把模型变成变量最后才动提示词。2. 一把 TaoToken Key 接管模型切换ChatOpenAI 与 LlamaIndex 的填写位置2.1 在模型广场复制 ID别为每个模型单开一套账号准备材料只有两样一个 Key两份你想对照的模型 ID。打开 TaoToken 注册并进入控制台创建 API Key把YOUR_API_KEY这个占位符替换成你拿到的那串值。模型 ID 同样在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 的模型广场里复制以当时列表显示的为准——不要照抄别人博客里带日期后缀的写法那多半是过期的。这么做的好处是GPT 系和 Qwen 系共用同一个 Key、同一个 base_url对照实验里唯一的变量就只剩模型名。如果每个模型都要单独申请一套凭证、单独记一遍限速规则A/B 就变成两件麻烦事的叠加你大概跑两轮就放弃了。export TAOTOKEN_API_KEYYOUR_API_KEY export TAOTOKEN_BASE_URLhttps://taotoken.net/api注意 base_url 末尾不要带/v1填进工具的地址是https://taotoken.net/api这个形式。2.2 LangChainbase_url 指向 https://taotoken.net/apiLangChain 侧用langchain-openai的ChatOpenAI就能接关键是base_url和api_key两项。import os from langchain_openai import ChatOpenAI BASE_URL os.environ[TAOTOKEN_BASE_URL] # https://taotoken.net/api def build_llm(model_id: str, temperature: float 0.2) - ChatOpenAI: return ChatOpenAI( modelmodel_id, api_keyos.environ[TAOTOKEN_API_KEY], # 即 YOUR_API_KEY base_urlBASE_URL, temperaturetemperature, timeout60, max_retries2, )这段代码里没有硬编码的模型名model_id由调用方传入。这就是整个改造的核心——后面切模型改的是调用参数不是文件。2.3 LlamaIndexapi_base 是同一个位置LlamaIndex 走OpenAILike参数名换成api_base值不变。import os from llama_index.core import Settings from llama_index.llms.openai_like import OpenAILike def configure_llm(model_id: str): Settings.llm OpenAILike( modelmodel_id, api_baseos.environ[TAOTOKEN_BASE_URL], # https://taotoken.net/api api_keyos.environ[TAOTOKEN_API_KEY], is_chat_modelTrue, temperature0.2, timeout60, )如果你的 Harness 里同时有检索器和 Agent ExecutorSettings.llm换成哪个模型检索后的合成、工具参数生成都会跟着换不用逐个组件改。2.4 把模型清单写成字典对照时只改一行import os MODELS { # 模型 ID 以模型广场当时列表为准别照抄示例 baseline: os.environ.get(MODEL_A, YOUR_GPT_MODEL_ID), candidate: os.environ.get(MODEL_B, YOUR_QWEN_MODEL_ID), }字典的 key 是实验角色value 是真实模型 ID。跑对照的时候MODELS[candidate]换成另一个 ID 就行系统提示词一个字都不用动。3. 把 Harness 系统提示词拆成四段才定位得到失效点3.1 角色段跨模型最容易漂的一层角色段的问题不在写得不够详细而在写得太依赖语境默认。像你是一位经验丰富的技术负责人善于从混乱的信息中提炼关键问题这种句子人读起来很顺模型执行时却没有任何可检验的抓手。换成可检验的写法改动其实很小把身份、服务对象、职责边界拆成三句具体的陈述再补一句当用户请求超出上述范围时直接说明超出范围并给出替代建议。后面这句是给模型一个明确的退出动作避免它在不确定时自由发挥。对照测试时角色段是最值得单独拿出来测的部分设计两三道越界提问看两个模型是否都按同一套边界拒绝。拒绝的措辞可以不同但行为必须一致。3.2 约束段写禁止比写尽量更稳约束条款的效果跟它在提示词里的位置和句式强相关。经验是越靠近输出契约的约束执行率越高用肯定句描述的约束比否定句更容易跨模型保留。举个例子尽量不要编造数据这种写法在 GPT-4 上通常没问题它会把尽量理解成硬约束通义千问可能把它理解成一种倾向在上下文压力大时让位给用户想快点拿到答案。改成当缺少数据时只输出MISSING_DATA并说明缺哪个字段不得用推测值填充行为就稳定多了。约束段里还有一个容易被忽略的点约束冲突。系统提示词里如果同时写着回答尽量简洁和每步都要给出完整推理依据两个模型对谁优先的判断可能不同。做 A/B 前先自查一遍有没有这种互斥条款否则你测出来的是模型的优先级偏好不是提示词的兼容性。3.3 推理规则段把顺序写成编号步骤而不是暗示Harness Engineering 里最核心的部分是让 Agent 按固定顺序工作先查工具、再校验结果、最后组织输出。GPT-4 对这种隐含的流程暗示很敏感一句在给出结论前请确认你已核对过相关资料它就能自动补出中间步骤通义千问更倾向于按字面执行暗示不写出来就不做。所以推理规则段要改成显式的编号流程判断用户问题是否命中已有检索工具命中则先调用未命中则直接说明。工具返回后核对返回内容是否包含回答所需字段缺失则停止并输出缺失项。仅在核对通过后组织最终答案答案中引用到的每条事实都要能对应到工具返回。编号步骤还有一个额外好处出问题时你能精确定位是第几步断的而不是笼统地说它没按流程走。提示如果 Harness 里挂了数据库或运维类工具把工具定位成生成 SQL / 生成命令的角色真正的执行放在本地由人来跑再把执行结果贴回对话。让模型直接对生产库动手是另一类风险跟提示词调优无关。3.4 输出契约段给结构别给形容词请用清晰的格式返回是无效契约。有效契约长这样{ status: ok | missing_data | out_of_scope, answer: string, evidence: [string], missing_fields: [string] }把这段 schema 直接贴进系统提示词再补一句只输出 JSON不要 markdown 围栏不要额外解释。切到通义千问之后如果它仍然习惯性包一层围栏你不必改提示词在解析层做一次兼容剥离就行——这是结构问题不是理解问题处理成本很低。3.5 用分层拼装替代一次性大字符串把四段拆开存拼装时按固定顺序合并PROMPT_PARTS { role: ROLE_TEXT, constraints: CONSTRAINT_TEXT, reasoning: REASONING_TEXT, output: OUTPUT_CONTRACT, } def build_system_prompt(parts, order(role, constraints, reasoning, output)): return \n\n.join(parts[key].strip() for key in order)拆开之后A/B 时你可以只替换reasoning一段其余三份保持完全一致。这样一次只验证一个假设结论才站得住。4. 同一把 Key 跑 A/B固定输入、锚定采样参数、只留一个变量4.1 采样参数不对齐对照就白做模型切换测试里最常见的错误是两个模型用了两套默认参数。有的默认温度 0.7有的默认 0.3跑出来的差异里混着随机性你根本分不清是提示词不兼容还是采样太散。稳妥做法是显式传参temperature0.2、top_p不动、max_tokens给同样的上限。两次运行都用这套参数模型的个性差异才被压到最小。如果业务本身需要高温采样那就把温度也当成一个实验维度单独测不要混在模型切换实验里。4.2 用固定 case 集不用随机问句拿几个临时想的问句去测结果没有可比性。准备一组 8 到 12 条的固定 case覆盖前面提到的三类崩法越界提问、信息缺失、多步推理、需要工具调用的场景。每条 case 存进文件输入完全相同。CASES [ {id: scope_01, input: 帮我处理一个明显超出职责范围的请求……, expect: out_of_scope}, {id: missing_01, input: 根据给定的订单号给出结论订单号未提供, expect: missing_data}, {id: flow_01, input: ……需要先检索再回答的问题……, expect: ok}, ]expect字段不用写得特别精确能表达该拒绝该报缺失该正常回答就够。它是给你自己看的判据不是自动化断言。4.3 一份可复制的对照脚本import json import time from build_llm import build_llm from build_prompt import build_system_prompt, PROMPT_PARTS SYSTEM_PROMPT build_system_prompt(PROMPT_PARTS) def run_one(llm, case): messages [(system, SYSTEM_PROMPT), (human, case[input])] start time.time() resp llm.invoke(messages) return { id: case[id], text: resp.content, latency: round(time.time() - start, 2), } def run_suite(model_id, cases): llm build_llm(model_id, temperature0.2) return [run_one(llm, c) for c in cases] if __name__ __main__: results { baseline: run_suite(MODELS[baseline], CASES), candidate: run_suite(MODELS[candidate], CASES), } with open(ab_result.json, w, encodingutf-8) as f: json.dump(results, f, ensure_asciiFalse, indent2)跑完会得到一份并列结果。别急着看总评分逐条比。4.4 结果怎么看按段归因不要整体重写对比时按这个顺序看两条 case 里是不是同一条失败了如果 baseline 也没过那说明这条 case 本身设计有问题先从对照集里剔掉。只有 candidate 失败且失败形态是没按流程走——指向推理规则段。只有 candidate 失败且形态是没有拒绝越界请求——指向约束段。两边行为一致但措辞差异大——不用改这是模型风格不影响 Harness。按段归因的好处是改动量可控。一次只调一段重跑对照确认这一段修好了再动下一段。整体重写会把已经调好的部分一起弄乱而且下次再换模型时你又要从零开始。5. 切到通义千问后常见的报错与软失败排查顺序5.1 配置类报错401、模型名不存在、404现象大概率原因处理方式401 UnauthorizedKey 拼错或带着多余空格重新从 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 复制一遍 Key模型不存在或不可用模型 ID 抄错或该 ID 已下架去模型广场核对当期列表404base_url 多写了/v1或末尾斜杠统一写成https://taotoken.net/api这三类都是配置问题跟提示词无关。先跑通一次最简单的单轮对话确认通道没问题再开始做提示词对照。否则你会在模型是不是不理解我的提示词上浪费半天实际是 Key 里多了一个换行符。5.2 软失败格式带围栏、工具参数缺失、拒绝行为改变软失败不会报错只会让 Agent 悄悄变差。三个最值得盯的点返回值被 markdown 围栏包住。解析层加一次剥离比改提示词可靠先判断首行是否是三反引号是则去掉首尾围栏再json.loads。工具调用参数缺字段。在调用工具前加一道参数校验缺字段就要求模型补齐而不是把半成品参数传给下游。这个校验在 GPT-4 上可能一直没触发过换模型后才开始频繁命中。拒绝行为改变。原来会明确拒绝的请求现在变成模糊回应。这类问题只能靠固定 case 集发现单次对话看不出来。发现后优先在约束段补一句显式的退出动作而不是在业务代码里加关键词过滤。5.3 排查顺序清单按这个顺序走能少绕路单轮对话能否跑通验证 Key、base_url、模型 ID。采样参数是否两边一致。推理规则段是否写成了编号步骤。输出契约是否给了具体 schema。约束段是否存在互斥条款。只有到这一步才考虑给某个模型写覆盖层。大多数换模型就崩的案例在第 3 步和第 4 步就解决了。6. 把调好的提示词固化分模型覆盖层加回归用例6.1 base prompt 加 model patch 的两层结构调好之后不要把结果散在测试脚本里。做成两层BASE_PARTS { role: ROLE_TEXT, constraints: CONSTRAINT_TEXT, reasoning: REASONING_TEXT, output: OUTPUT_CONTRACT, } MODEL_PATCH { YOUR_QWEN_MODEL_ID: { reasoning: REASONING_TEXT_STRICTER, }, } def build_for_model(model_id): parts dict(BASE_PARTS) parts.update(MODEL_PATCH.get(model_id, {})) return build_system_prompt(parts)只有确实需要差异化的那一段写进 patch其余全部沿用 base。补丁越薄维护成本越低将来上新模型时也只需要加一条新条目不用复制一整份提示词。6.2 留一组跨模型回归用例把第 4 章那批 case 保留下来每次改提示词或加新模型时跑一遍。不用搭复杂的评测框架一份 JSON 结果文件加人工扫一遍就够用。重点是用例要长期保留而不是每次临时想几个问题。跑完之后你会有一份跨模型的横向记录同一个 case 在哪几个模型上稳定、在哪几个上失败。这份记录比任何评测分数都实在因为它是你自己业务的真实场景。6.3 回控制台对一下这次切换有没有记上账配置落地后先用同一把 Key 在 TaoToken 模型对话 里发一条测试消息确认模型 ID 和 base_url 没填错也顺便看看两个模型返回的风格差异。接着回到 控制台 API Keys 核对一下刚才那几轮对照调用有没有正常计入用量顺便确认 Key 没有多余的权限。如果你打算把这套 Harness 长期挂在写代码的流程里跑可以顺手看看 Coding Plan 的套餐够不够用把 Agent 放进编辑器、用环境变量接管模型调用的完整对照见 Claude Code 接入文档。最后留一句提醒提示词调优这件事真正省时间的不是把一段话改得更漂亮而是让每次改动都只对应一个变量。模型切换成本降下来之后你才有耐心一条一条跑对照而不是靠感觉来回改。