在 AI 应用圈子里流传过一句高管自嘲:有公司负责人给自己封了个头衔,叫“首席大脑损伤官”。这句话虽然是个玩笑,但它点出的问题非常真实——大语言模型应用上线之后,负责人每天要处理的往往不是模型有多聪明,而是它会在哪里突然犯傻:一本正经地编造来源、把用户的问题理解偏、同一句话换个说法就给出完全不同的答案。任何一条被用户抓到,都意味着返工、解释、改 prompt、重新评测。
这篇文章不是八卦新闻,而是围绕“如何治理大模型应用的不可靠输出”写的一篇工程实践记录。通篇会讲清楚:幻觉是怎么产生的,为什么光靠提示词摁不住,评估集怎么建,自动化回归怎么做,线上监控要盯哪些信号,以及为什么评测都通过了,上线还是会翻车。读完你手里会有一套可以落到项目里的最小方案,而不是一堆概念。
1. 先从“首席大脑损伤官”这个自嘲说起:LLM 应用的失控点到底在哪
1.1 幻觉不是 bug,而是语言模型的默认行为
很多第一次接触大模型应用的人会默认“模型是在查数据库”,所以当模型说出一个不存在的论文标题、虚构一个 API 参数、编造一个公司名称时,第一反应是“它坏了”。
实际上大语言模型的本质是一个概率化的文本生成器。它做的每一件事都是预测“下一个 token 最可能是什么”,而不是“哪一条事实最正确”。模型内部确实存下了大量训练数据里的统计规律,但这些规律不等于事实库,更不等于经过审核的知识库。只要某个说法在训练数据里出现过足够多次、语言模式又通顺,模型就有概率把它当成正确答案输出来。
这就是“脑损伤”的根源:模型没有“查证”这个动作。它在生成答案时不会去翻资料,只会根据上下文和已有参数继续写下去。所以幻觉并不是某个模型特有的缺陷,而是这类架构的默认行为。理解了这一点,后面的工程防御才有意义。
1.2 三类最容易让开发者崩溃的异常输出
把问题归拢一下,日常开发里最伤人的输出异常大概有三种。
第一类是事实幻觉。模型编造不存在的引用、数据、法规条款、历史事件。这类问题在客服、金融、医疗、法律场景下最危险,因为用户无法判断哪一段是编的。
第二类是格式漂移。你明确要求“只输出 JSON”,模型却在开头加了一段“好的,我来为您解答”,或者在 JSON 外面包了一层 Markdown 代码块。结构化输出一旦失败,下游解析直接报错,程序层面比事实错误更容易暴露。
第三类是语义不稳定。同样一个问题,换一种说法、换一轮对话,模型给出的结论就变了。这个问题在评测时最隐蔽,因为人工抽检十次可能只有一次不同,难以复现。
| 异常类型 | 典型现象 | 直接后果 | 主要治理手段 |
|---|---|---|---|
| 事实幻觉 | 编造来源、数据、条款 | 用户信任受损,业务出错 | RAG 锚定、引用可验证、拒答 |
| 格式漂移 | 要求 JSON 却输出自然语言 | 下游解析失败 | 结构化输出、Schema 约束、后处理校验 |
| 语义不稳定 | 换一种问法结论就变 | 体验不一致,评测难以复现 | 温度下调、上下文整理、回归评测 |
1.3 治理目标不是让模型不犯错,而是让错误能被系统拦住
这里要先纠正一个预期:你不可能通过写 prompt 让模型“永远正确”。模型一定会犯错,真正要做的,是把错误控制在系统里,让错误不会直接暴露给用户。
这句话拆开是三件事。第一,给模型提供可靠资料,让它在回答时尽量引用资料而不是凭记忆生成。第二,给模型的输出加校验,格式不对、缺少引用、命中敏感词,就在程序层拦截。第三,对模型不确定的内容设置“不知道就直说”的策略,而不是为了讨好用户硬编一个答案。
这三件事合起来,就是一套防“脑损伤”的最小框架:评估集、护栏、自动化回归、线上监控。下面逐一展开。
2. 先建评估集:没有“参考答案”,就谈不上防脑损伤
2.1 最小评估集怎么设计
很多团队做 LLM 应用时,最大的问题不是没有 prompt,而是没有评估集。跑几个例子觉得效果不错就上线了,等到用户反馈问题,才发现不知道该拿什么标准去衡量“到底坏了没有”。
评估集是治理不可靠输出的第一步。它不需要一开始就很大,但必须覆盖三种情况:正常问题、边界问题、应该拒答的问题。
正常问题,指业务里最常见的高频问题,用来确认基础能力没有退化。边界问题,指问题里混入不相关信息、问题本身表述模糊、一个问题是多选一等情况,用来确认模型不会轻易被绕晕。应该拒答的问题,指模型没有资料支撑、超出业务范围、用户要求猜测敏感信息等情况,用来确认模型敢说“不知道”。
下面是一个最小评估集的 JSON 示例,每个条目都带上了业务分类和检查要求。
[ { "id": "t001", "category": "normal", "question": "通过 API 创建订单时,token 过期会返回什么错误码?", "reference_answer": "返回 401,错误码为 TOKEN_EXPIRED。", "must_mention": ["401", "TOKEN_EXPIRED"], "should_reject": false }, { "id": "t002", "category": "boundary", "question": "如果用户补全了缺失的必填字段,是不是就一定能下单成功?", "reference_answer": "不是,还需要校验库存、风控和支付状态。", "must_mention": ["库存", "风控"], "should_reject": false }, { "id": "t003", "category": "reject", "question": "根据经验猜一下,明年订单量大概会增长多少?", "reference_answer": "拒绝回答,不能凭空预测。", "must_mention": [], "should_reject": true } ]设计评估集时有几点要注意。
条目数量不能太少,建议每个核心业务场景至少 20 到 50 条,否则一次提示词改动的副作用很难体现出来。条目内容要贴近真实用户提问,不能只写标准问法,要写用户真的会说的那种口语化、错别字、上下文缺失的问题。每个条目必须带“该不该答”的标记,很多团队只测“答得好不好”,忘了测“该不该拒答”,结果模型为了通过评测,什么问题都硬答。
2.2 用评估脚本批量跑模型
评估集建好之后,要写脚本批量跑,不要靠人工一条条复制到对话窗口里问。人工测试最大的问题是不稳定,同一个问题多问几次结果都不一样,你很难判断到底是模型坏了还是玩法变了。
下面是一个用 Python 写的批量推理脚本,假设模型服务提供 OpenAI 兼容接口。
import json import time from openai import OpenAI client = OpenAI( base_url="http://localhost:8000/v1", api_key="local-test-key", ) SYSTEM_PROMPT = ( "你是订单查询助手。只能根据提供的资料回答," "资料中没有的内容要明确说不知道,不要猜测。" ) def run_case(client, case, temperature=0.2): start = time.time() resp = client.chat.completions.create( model="your-model-name", temperature=temperature, messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": case["question"]}, ], ) answer = resp.choices[0].message.content latency = time.time() - start return {"id": case["id"], "answer": answer, "latency_ms": round(latency * 1000, 2)} def main(): with open("eval_set.json", "r", encoding="utf-8") as f: cases = json.load(f) results = [] for case in cases: result = run_case(client, case) result["category"] = case["category"] result["should_reject"] = case["should_reject"] result["must_mention"] = case["must_mention"] results.append(result) with open("eval_result.json", "w", encoding="utf-8") as f: json.dump(results, f, ensure_ascii=False, indent=2) print(f"completed {len(results)} cases, saved to eval_result.json") if __name__ == "__main__": main()这段脚本的关键点有三个。
把温度参数设成固定值,建议 0 到 0.3。评测不是为了看模型的“创造力”,而是为了看它在一个相对稳定的生成策略下能不能答对,温度太高会让同一个问题每次结果都不一样,评测结果失去参考意义。每次评测使用同一个系统提示词,prompt 文本必须和线上一致,否则你测的是另一个系统的能力。结果落盘到 JSON 文件,方便后面做指标统计和留存归档。
2.3 指标怎么算
拿到批跑结果之后,要算几个可量化指标,而不是只靠“读一遍感觉还行”。
| 指标 | 计算方式 | 建议阈值(起步阶段) | 说明 |
|---|---|---|---|
| 必提词命中率 | 命中 must_mention 的条数 / 正常回答总条数 | 不低于 90% | 验证关键信息有没有被覆盖 |
| 拒答触发率 | 实际拒答条数 / 应该拒答条数 | 不低于 95% | 防止模型硬编答案 |
| 误拒率 | 不该拒答却拒答的条数 / 不该拒答总条数 | 不高于 5% | 防止模型过度保守 |
| JSON 格式合规率 | 输出可被 json.loads 解析的条数 / 结构化输出总条数 | 不低于 98% | 防止格式漂移 |
| 平均响应延迟 | 所有条目的 latency_ms 平均值 | 按业务要求 | 评估集也能暴露性能退化 |
必提词命中率是最容易落地的语义检查。它不要求模型回答得和参考答案一模一样,只要关键实体和关键概念出现,就算过关。这种方式实现简单,也足够用来做回归。
拒答触发率要配合 should_reject 字段使用。很多模型在“回答不了”的时候会继续编,通过这个指标能很快发现哪些问题需要补检索资料、哪些提示词需要强调边界。
3. 给模型加护栏:从 prompt 约束到 RAG 锚定
3.1 系统提示词要写清边界和输出格式
评估集告诉你怎么发现问题,护栏负责在生成阶段减少问题。第一道护栏就是系统提示词。
很多团队写的系统提示词只有一句话,比如“你是智能客服”。这远远不够。一个合格的系统提示词至少要包含三块内容:角色和任务、回答边界、输出格式要求。
下面是一段可参考的提示词模板,实际项目里要按业务调整。
你是订单查询助手,负责解答用户关于订单状态、支付、物流和退款的问题。 回答规则: 1. 只能依据资料库中提供的资料回答,禁止凭训练记忆补充事实。 2. 资料中没有的信息,明确回答“没有查到相关记录”,不要猜测。 3. 不要回答与订单查询无关的问题,引导用户回到业务范围。 4. 不要输出任何金额以外的数字预测、时间预测或承诺性结论。 5. 返回内容必须是 JSON,字段为 {"answer": "回复正文", "references": [来源编号]}。这里要解释一个容易误解的点:系统提示词不是“法律”,模型不一定会遵守。它只是提高模型行为符合预期的概率。所以写完提示词之后,一定要靠评估集去验证,而不是自己读一遍觉得“写得挺清楚”就上线。
3.2 用结构化输出约束格式
如果模型服务支持 JSON Schema 或结构化输出,一定要用,不要只靠提示词里写“返回 JSON”。
OpenAI 兼容接口里,多数服务支持 response_format 参数。下面是一个示例。
resp = client.chat.completions.create( model="your-model-name", temperature=0.2, messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_question}, ], response_format={ "type": "json_schema", "json_schema": { "name": "order_chat_response", "strict": True, "schema": { "type": "object", "properties": { "answer": {"type": "string"}, "references": {"type": "array", "items": {"type": "string"}} }, "required": ["answer", "references"], "additionalProperties": False } } }, )使用结构化输出有三个好处。
格式稳定,模型不再容易输出多余解释文字或 Markdown 代码块,下游解析少一层负担。字段约束明确,required 字段缺失时接口会直接报错,能在程序层暴露问题。方便后续校验,answer 字段可以做长度检查和拒答判断,references 字段可以逐个校验来源编号是否存在。
要注意:结构化输出只保证格式合法,不保证内容正确。模型依然可能输出结构合规但内容错误的 JSON,所以格式校验之后仍然要做内容层面的评估和抽检。
3.3 用 RAG 把回答锚定在可靠资料上
RAG(检索增强生成)是目前对抗事实幻觉最主流的方案。思路很简单:模型回答问题之前,先从自己的资料库里检索相关片段,把片段拼进上下文,让模型基于片段作答。
它的价值在于从根本上改变了模型的“信息来源”——以前模型凭训练记忆回答,现在模型面对的是明确的、可追溯的文档片段。资料里有就答,资料里没有就拒答,模型编造的空间被大幅压缩。
一个最小 RAG 流程是这样的:
def build_context_with_retrieval(question): chunks = retrieval_service.search(question, top_k=3) context_parts = [] for rank, chunk in enumerate(chunks, start=1): context_parts.append( f"[{rank}] 来源:{chunk['doc_id']}\n{chunk['content']}" ) return "\n".join(context_parts)检索结果放进用户消息之前,要加一层来源编号。这样模型在回答里引用“根据资料 1”时,系统才能把模型输出里的引用编号和真实文档对应起来,做引用可验证。
RAG 落地时最容易忽视的是召回质量。经常出现的问题有三个:检索没召回相关资料,模型变成无资料硬答;召回了多个互相矛盾的片段,模型无所适从;召回内容本身是过期的,模型答得越准确错得越离谱。
所以 RAG 不是“加上检索就万事大吉”,它要求你对资料库做持续维护:清洗文档、切分片段、建立索引、定期更新、标注有效时间范围。
3.4 设置“不知道就直说”和拒绝阈值
幻觉治理的最后一道生成侧护栏,是教会模型拒绝。
很多模型被训练成“讨好用户”的模式,用户问什么,它就尽量接话。要打破这个模式,必须在系统提示词里明确写:资料不足时禁止猜测,可以回答“没有查到相关记录”。
在 RAG 场景下,还可以加一个程序层的拒绝逻辑。比如检索结果的相关性分数低于设定阈值,就不要把结果交给模型生成,而是直接返回“资料库中没有找到相关信息”。
retrieved = retrieval_service.search(question, top_k=3) if not retrieved or retrieved[0].score < 0.45: return {"answer": "没有查到相关记录,请补充订单号后重试。", "references": []}这个阈值的设定不能拍脑袋。阈值太高,大量问题都会被误拒,用户体验变差;阈值太低,护栏形同虚设。正确做法是用评估集里的 should_reject 和误拒率两个指标来调节,找到一个让误拒率不高于 5%、拒答触发率不低于 95% 的平衡点。
4. 把防“脑损伤”做成自动化回归
4.1 评测脚本的完整结构
评估集和护栏都准备好了,接下来要解决一个工程问题:每次改 prompt、换模型、调参数,怎么快速知道效果是变好还是变坏。
答案是自动化回归。把第 2 节的批量推理脚本扩展成带校验逻辑的完整评测脚本。校验逻辑包含三部分:格式校验、必提词校验、拒答校验。
import json def validate_format(answer): try: parsed = json.loads(answer) return parsed.get("answer"), parsed.get("references", []) except Exception: return None, None def check_must_mention(text, must_mention): if not must_mention: return True return all(keyword in text for keyword in must_mention) def check_reject(text, should_reject): has_reject = ("没有查到" in text) or ("无法回答" in text) return has_reject if should_reject else not has_reject def evaluate_one(result, case): parsed_answer, references = validate_format(result["answer"]) if parsed_answer is None: return {"id": case["id"], "pass": False, "reason": "json_parse_error"} checks = { "must_mention": check_must_mention(parsed_answer, case["must_mention"]), "reject_policy": check_reject(parsed_answer, case["should_reject"]), "references_valid": all(ref in allowed_references for ref in references), } passed = all(checks.values()) return { "id": case["id"], "pass": passed, "checks": checks, "reason": "" if passed else json.dumps(checks, ensure_ascii=False), }这段代码里的 allowed_references 需要根据实际资料库来维护,可以用文档 id 白名单替代。引用校验的意义在于确认模型没有编造资料编号,这是事实幻觉的一个直接信号。
4.2 用阈值和失败规则拦住坏版本
评测脚本跑完,要算总体通过率,并设置硬性失败规则。通过率不是“看个热闹”,而是发布流程里的判断依据。
REQUIRED_PASS_RATE = 0.95 total = len(report_results) passed_count = sum(1 for item in report_results if item["pass"]) pass_rate = passed_count / total print(f"pass_rate={pass_rate:.2f}, passed={passed_count}, total={total}") if pass_rate < REQUIRED_PASS_RATE: print("RESULT=FAIL") raise SystemExit(1) print("RESULT=PASS")用 exit code 表示评测结果,是为了把评测接进 CI。任何一次 prompt 修改、模型版本切换、RAG 配置变更,只要让通过率跌破阈值,就应该被视为发布阻断问题,而不是可接受的小波动。
4.3 把评测接入发布流程
在 CI 里加一步评测任务,通常的做法是在发布流水线的“测试”阶段后、部署之前,增加一个评测 job。
evaluate-llm: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install dependencies run: pip install openai requests - name: Run LLM evaluation run: python scripts/evaluate.py env: MODEL_ENDPOINT: ${{ secrets.MODEL_ENDPOINT }} MODEL_API_KEY: ${{ secrets.MODEL_API_KEY }}接入 CI 之后要特别注意环境一致性。本地评测和 CI 评测必须连同一个模型服务、使用同一个模型版本、加载同一份评估集。很多团队在本地测是好的,CI 一直失败,查了半天发现是本地的评估集已经被改过,CI 里还是旧版。
4.4 报告里至少要留有这些字段
评测结果要落盘、留档,不要只打印在控制台里。至少保留以下字段,方便做时间维度上的对比。
| 字段 | 示例 | 用途 |
|---|---|---|
| run_id | 20250612-1530 | 标识一次评测 |
| model_version | your-model-name-v20250610 | 区分模型版本 |
| eval_set_version | v12 | 区分评估集版本 |
| prompt_version | prompt-v8 | 区分系统提示词版本 |
| pass_rate | 0.97 | 总体通过率 |
| failure_items | t003, t008 | 定位失败用例 |
| generated_at | 2025-06-12 15:30:00 | 归档时间 |
有了这些字段,你才能回答“上上个版本明明是好的,这周怎么挂了”这类问题。
5. 线上监控:上线之后才是“脑损伤”高发期
5.1 记录输入输出和关键上下文
自动化评测管的是发布前,线上监控管的是发布后。很多模型问题只在真实流量下出现,因为真实用户不会像评估集那么“讲礼貌”。
线上系统至少要记录一份请求日志。字段包括用户问题、模型最终输出、模型原始输出(如果有差异)、检索来源片段、引用编号、延迟、模型版本、提示词版本。
{ "timestamp": "2025-06-12T15:30:01.123Z", "request_id": "req_8f3a2", "user_question": "我的订单为什么还没发货?", "model_version": "your-model-name-v20250610", "prompt_version": "prompt-v8", "answer": "没有查到相关记录,请提供订单号。", "references": [], "retrieved_docs": ["doc_order_0451", "doc_order_1289"], "latency_ms": 812, "status": "answered" }这份日志是后面所有排查工作的数据基础。没有日志,线上出了问题就只能靠用户截图,排查效率会非常低。
5.2 抽检和用户反馈闭环
线上日志的量大到不可能每条都人工审核,所以要设计两类反馈机制。
第一类是用户反馈。在对话窗口提供“有帮助/没帮助”的按钮,把反馈结果回写进会话记录。用户点“没帮助”的会话,就是接下来人工抽检的优先级最高的样本。
第二类是人工抽检。定期从日志里按比例抽样本,比如每 1000 条回答里抽 50 条,由业务人员判断回答是否准确。抽检结果要有标记系统,标出“事实错误”“答非所问”“格式错误”“正常”,再把标记结果回流成新的评估集条目。
SELECT request_id, user_question, answer FROM llm_request_log WHERE user_feedback = 'negative' ORDER BY created_at DESC LIMIT 100;这条 SQL 可以快速拉出最近被用户点“没帮助”的对话,从最痛的问题开始查。
5.3 监控哪些信号
线上监控不能只盯着 CPU 和内存,模型应用的可观测性要围绕“模型行为是否符合预期”来设计。
| 监控信号 | 正常表现 | 需要警惕的表现 | 建议处理方式 |
|---|---|---|---|
| 拒答率 | 平稳,符合业务预期 | 突然上升或下降 | 检查资料库更新、阈值配置 |
| JSON 解析失败率 | 低于 1% | 批量上升 | 检查模型版本、response_format 配置 |
| 输出长度 | 在业务设定范围内 | 突然变短或变超长 | 检查 prompt、上下文拼接 |
| 平均延迟 | 稳定 | 持续升高 | 检查上下文长度、检索耗时 |
| 用户重试率 | 平稳 | 上升 | 抽查重试会话,定位不满原因 |
| 引用编号无效率 | 接近 0 | 快速上升 | 检查资料库 id 是否被删除 |
这些信号里,拒答率和 JSON 解析失败率最值得优先看。一个是内容侧的“防线”,一个是程序侧的“格式防线”,任何一个失守都会直接造成用户体验问题或系统报错。
6. 常见坑:为什么评测通过,上线还是翻车
6.1 评估集和真实用户问题分布不一致
现象:线下评测通过率 97%,线上用户还是说答非所问。
原因:评估集里的问题太“标准”了。真实用户会带着错别字、口语、缺失上下文来提问,比如只发一个订单号没有说明意图,或者一句话里夹杂两个问题。评估集没有覆盖这些形态,评测结果自然不代表线上水平。
解决方式:定期从线上日志抽取真实用户问题,清洗后加入评估集。评估集要贴近真实分布,而不是只反映“理想提问方式”。
6.2 prompt 写了规则但模型不遵守
现象:系统提示词里明明写了“禁止猜测”,模型还是编了一个答案出来。
原因:提示词只是概率性约束,不是硬性规则。模型训练目标决定了它倾向于生成流畅、完整的回答,“不知道”这种表达会降低自然度,模型可能为了流畅性牺牲规则。
解决方式:不能只靠写提示词,要在程序层做校验。模型输出后检查是否命中“没有查到”这类拒答标记,如果输出的引用为空但业务上必须引用,直接把这条回答降级为拒答。规则写在 prompt 里,校验必须写在代码里。
6.3 RAG 召回的是过期内容
现象:资料库更新了,模型回答还是旧政策。
原因:检索索引没有同步更新,或者旧的文档片段仍然存在索引里,和新文档混在一起。模型同时看到新旧两版说法,可能采取“哪个更像正确答案”的策略,结果选错。
解决方式:建立资料库版本机制,文档更新时同步重建或增量更新索引;给文档片段标注生效时间,检索时按时间过滤;每次资料库变更后,跑一遍评估集里的相关业务条目。
6.4 温度参数调得太高或太低
现象:线上模型回答同一个问题两次,结论不一样。
原因:温度偏高,模型在每次采样时可选 token 的随机性变大,直接影响结论稳定性。尤其在评测阶段,如果评测脚本没有固定温度,两次评测结果无法对比。
解决方式:业务对话场景建议温度设为 0 到 0.3。需要抽取创意见解、文案改写时再考虑提高。评测脚本里必须显式传 temperature,不能依赖服务端默认值。
6.5 只测单轮,不测多轮
现象:单轮评测全过,用户连续追问两三句之后,模型开始乱答。
原因:很多评估集只包含“一问一答”的单个条目,没有模拟多轮对话。多轮场景下,模型需要同时处理历史消息、新问题意图识别、指代消解,任何一个环节出错都会导致最终答案偏离。
解决方式:评估集里增加多轮会话条目,每个条目包含一组 messages,而不是单条 user 消息。评测脚本要把完整消息列表发给模型,检查最终回答是否仍然正确。
下面是几个高频问题的排查链路,按顺序查可以少走弯路。
| 现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 评测通过,线上翻车 | 评估集与用户分布不一致 | 对比线上日志抽取的问题类型 | 把真实用户问题回流进评估集 |
| 输出出现编造内容 | prompt 边界不够明确或 RAG 未命中 | 查看检索片段是否为空、相关性分数 | 补资料、加拒答逻辑 |
| 格式化输出失败 | response_format 未配置或模型版本不支持 | 检查请求参数和模型文档 | 使用结构化输出并做解析校验 |
| 线上偶发错误回答 | 温度过高 | 检查服务端温度配置 | 固定为 0 到 0.3 |
| 多轮对话答偏 | 未传历史消息或压缩逻辑丢失关键信息 | 查看请求 messages 结构 | 保留必要历史,按窗口截断 |
7. 最佳实践与可复用清单
7.1 学习环境与生产环境的差异
这篇文章前面给的脚本和配置,追求的是“最小可运行”。如果只在本地学习,跑通一条流程就够了。但要搬到生产环境,还需要补齐不少东西。
| 维度 | 学习/开发环境 | 生产环境 |
|---|---|---|
| 配置 | 代码里写死模型地址和 key | 通过环境变量或配置中心注入,密钥保密 |
| 日志 | 控制台打印即可 | 结构化日志输出到日志平台,保留时间序列 |
| 评估集 | 50 条以内说明思路即可 | 按业务模块拆分,持续扩充,版本管理 |
| 监控 | 无 | 接入指标、告警、抽检闭环 |
| 资料库 | 静态测试文档 | 定时更新、版本管理、质量检查 |
| 回滚 | 手动重跑 | 模型版本和 prompt 版本可回退 |
7.2 发布前检查清单
每次发布一个模型应用新版本,建议按下面的清单走一遍。清单不需要一次全部建完,可以分阶段补上。
- 评估集是否有新增真实用户问题,版本号是否更新。
- 本次改动是否跑过完整回归,通过率是否达到阈值。
- 审查失败用例,确认失败原因已定位,不是随机波动。
- 检查资料库是否同步更新,过期内容是否移除。
- 确认温度参数固定,结构化输出配置已生效。
- 检查系统提示词和线上配置一致,没有本地改过未提交。
- 确认日志字段已包含模型版本、prompt 版本、检索来源。
- 确认拒答率和 JSON 解析失败率告警已配置。
- 准备回滚方案:上一版本是否可快速切换,相关配置是否留档。
- 指定抽检负责人,明确上线后 24 小时内人工抽检的样本范围。
7.3 扩展方向
如果这套最小方案已经跑通,下一步可以从三个方向加深。
评估集质量方向。引入更细的人工标注维度,比如“答案是否基于资料”“答案是否完整”“是否存在过度承诺”,用结构化标注取代简单的通过/不通过。还可以逐步加入多轮对话评测、跨语言评测、业务流任务完成率评测。
可观测性方向。把线上日志接到链路追踪系统,把模型输出、检索片段、引用校验结果、用户反馈串成一条 trace,出问题时可以一次性看到完整上下文。这个方向投入产出比较高,因为模型应用的很多问题只有看到完整链路才能定位。
门槛模型与护栏服务方向。当应用规模变大,可以在主模型之外增加一个小型校验模型,负责检查主模型输出的拒答合规、引用真实性、格式合法性。两个模型分工之后,评测和拦截的职责会更清晰,主模型的 prompt 也能简化。
回到开头那个自嘲。所谓“首席大脑损伤官”,本质上是把大模型应用不可控的那一面,变成了一个需要持续投入的工程问题。它没有一次性解法,但有一个明确的方法路径:建评估集、加护栏、做回归、看监控、持续回流问题。把这条路径跑起来,被模型气得“脑损伤”的次数会明显变少。真正值得投入精力的,不是追求“模型永远不出错”,而是让每一次错误都能被发现、被定位、被拦截,并且在下一次发版之前就被修复。