这是我们 AI 工程与 LLM 应用团队 13 个人耗时 87 天,把一套用了两年的"古老 LLM 应用体系 + prompt 字符串拼接散落各处无版本管理 + 直接调原始 API 无重试无超时无降级 + 全靠模型记忆胡编乱造幻觉满天飞 + 上下文硬塞超长被无声截断 + 无缓存重复 query 重复烧钱 + 同步阻塞调用用户干等几十秒 + 无流式输出整段憋着等 + 输出靠正则硬抠 JSON 经常解析失败 + 模型硬编码换个模型要改一堆代码 + 无评估靠人肉看效果全凭感觉 + 无护栏 prompt 注入和有害输出裸奔"的粗放 LLM 应用体系,整体重构到 2026 年"prompt 模板化版本化管理 + LLM 网关统一接入重试超时降级多模型路由 + RAG 检索增强生成消除幻觉 + 上下文窗口管理与压缩 + 语义缓存省钱 + 流式 SSE 输出 + 结构化输出 function calling + 自动化 eval 与 LLM-as-judge + 输入输出 guardrails 护栏 + token 用量延迟成本全链路可观测"现代 LLM 应用体系的真实战役复盘。重构前,我们的 AI 应用是典型的"prompt 改一处忘了同步另一处、模型一本正经地胡编、用户问一句要干等几十秒、输出格式动不动解析崩、上线了也不知道效果到底好不好"的危局;一次 prompt 注入就能让模型吐出不该说的话。重构后,我们用模板管住了 prompt、用 RAG 摁住了幻觉、用流式让等待可感知、用 eval 让效果可衡量、用护栏给输入输出上了保险。这 87 天里我们沉淀了 47 套工程修法、7 个 P0 事故复盘和 6 条工程哲学,本文毫无保留地分享出来。
需要先说明:LLM 应用现代化不是"换个更强的模型"这么简单——它是从"prompt 靠拼、知识靠模型硬记、调用靠裸调、效果靠感觉、安全靠运气"的粗放使用,跃迁到"prompt 工程化、知识靠检索增强、调用有韧性兜底、效果可自动评估、安全有护栏防护"的工程化应用的范式更替。下面这张表,概括了我们重构前后在十个核心维度上的对比,每一行背后都是数周攻坚。
| 维度 | 重构前(古老粗放使用) | 重构后(2026 现代工程化) |
|---|---|---|
| Prompt 管理 | 字符串拼接散落各处 | 模板化 + 版本化 |
| 知识获取 | 全靠模型记忆胡编 | RAG 检索增强生成 |
| 输出格式 | 正则硬抠 JSON 易碎 | 结构化 function calling |
| 响应方式 | 整段等几十秒 | 流式 SSE 输出 |
| 调用韧性 | 裸调无重试降级 | 网关重试超时降级 |
| 模型选择 | 硬编码换模型改代码 | 多模型路由可切换 |
| 成本控制 | 重复 query 重复烧钱 | 语义缓存命中复用 |
| 质量评估 | 人肉看全凭感觉 | 自动化 eval + LLM 评判 |
| 安全护栏 | 注入与有害输出裸奔 | 输入输出 guardrails |
| 可观测 | token 成本黑盒 | 用量/延迟/成本追踪 |
一、Prompt 工程化:从散落的字符串拼接到模板化版本化
重构的第一仗,是管住 prompt。古早时代我们的 prompt 就是散落在各个函数里的字符串拼接——"你是一个助手,请回答:" + user_input,同一个意图的 prompt 在好几处各写了一遍、措辞还略有不同,改一处忘了改另一处是家常便饭,更没有版本管理,谁也说不清线上现在用的到底是哪一版 prompt、上次改动让效果变好了还是变差了。现代做法是把 prompt 当作一等公民来工程化:用模板把可变部分参数化、把 prompt 集中管理并纳入版本控制,像管代码一样管 prompt。下面是 prompt 模板化的对比:
# 重构前:prompt 字符串拼接散落各处,同一意图好几份措辞不一,改一处忘改另一处
# prompt = "你是一个助手,请回答用户问题:" + user_input + "。要简洁。"
# resp = client.chat(prompt) # 散落、无版本、无法复用、改了不知影响哪里
# 重构后:prompt 模板化 + 集中管理 + 版本化,可变部分参数化,像管代码一样管 prompt
from string import Template
class PromptRegistry:
"""集中管理所有 prompt 模板,带版本号,纳入 Git 版本控制"""
QA_ANSWER_V2 = Template(
"你是$role。请基于以下检索到的资料回答用户问题,"
"资料中没有的内容不要编造,如实说明不知道。\\n"
"资料:$context\\n问题:$question\\n要求:$constraints"
)
def build_qa_prompt(question: str, context: str) -> str:
# 用模板渲染:可变部分作为参数传入,模板本身统一维护、单一来源
return PromptRegistry.QA_ANSWER_V2.substitute(
role="严谨的客服助手",
context=context,
question=question,
constraints="分点作答,每点不超过两句话",
)
# prompt 改版即改模板版本(V2→V3),配合 eval 对比新旧版本效果,改动可追溯可回退
Prompt 工程化让我们的 prompt 管理从"字符串拼接散落各处、同一意图多份措辞不一、改一处忘改另一处、无版本说不清线上是哪版"进化到了"模板化集中管理、可变部分参数化、纳入版本控制、改动可追溯可回退":过去 prompt 就是随手写在各个业务函数里的字符串拼接,同一个问答意图的 prompt 在好几个地方各拼了一份、连措辞和要求都略有出入,改 prompt 时改了这处忘了那处导致行为不一致是常事,而且根本没有版本概念,线上当前到底跑的是哪一版 prompt、上一次的改动究竟让效果变好还是变差了,谁也说不清楚、出了问题也无从回退;现在我们把 prompt 当成和代码一样重要的一等公民来对待,用模板把其中的角色、资料、问题、约束等可变部分参数化、把所有 prompt 模板集中到一处统一维护并纳入 Git 版本管理,每次 prompt 改版就是一次有版本号、有 diff、可评估、可回退的提交,配合后面的自动化 eval 还能定量对比新旧 prompt 版本的效果再决定是否上线。我们的纪律是"严禁在业务代码里散写拼接 prompt、一切 prompt 模板集中管理并版本化、prompt 改动必须配 eval 对比效果、可变部分一律参数化注入"。Prompt 工程化的本质认知是:prompt 是 LLM 应用里真正决定行为的'源代码'——它的好坏直接决定输出的质量,可它却常被当成无足轻重的随手字符串散落各处、既不复用也不版本化;把 prompt 提升为需要集中管理、版本控制、效果评估的一等工程资产,正是让 LLM 应用从'凭手感调字符串'的玄学,走向'可维护、可演进、可度量'的工程的第一步。
二、RAG:从模型硬记胡编到检索增强生成
第二仗,也是治理幻觉的关键一仗,是 RAG。古早时代我们直接拿用户问题去问模型,指望它靠预训练时记住的知识来回答——可模型的知识有截止日期、不知道你私有的业务数据、而且最要命的是它在不知道答案时不会说'不知道',而是一本正经地胡编乱造(幻觉),编得有鼻子有眼让人难辨真假,在客服、问答这种场景里这是致命的。RAG(检索增强生成)换了思路:先把你的知识库(文档、FAQ、数据)切块、用 embedding 模型转成向量存进向量数据库,用户提问时先把问题也转成向量、去向量库里检索出最相关的几段资料,再把这些资料连同问题一起喂给模型、让它'基于给定资料'回答。下面是 RAG 的核心流程:
# 重构前:直接问模型,靠它硬记的知识答,不知道就胡编(幻觉),私有数据它根本不知道
# answer = client.chat("我们的退款政策是几天?") # 模型瞎编一个"7天",其实是15天
# 重构后:RAG 检索增强——先检索私有知识库拿到真实资料,再让模型基于资料回答
def rag_answer(question: str) -> str:
# 1. 把问题转成向量
q_vec = embed_model.encode(question)
# 2. 去向量数据库检索最相关的 top-k 段资料(语义相似,非关键词匹配)
hits = vector_db.search(q_vec, top_k=5)
# 3. 可选:用重排模型(rerank)对召回结果再精排,把最相关的放前面
hits = reranker.rerank(question, hits)[:3]
context = "\\n".join(h.text for h in hits)
# 4. 把检索到的真实资料 + 问题一起喂给模型,约束它"只基于资料回答、别编"
prompt = build_qa_prompt(question=question, context=context)
return client.chat(prompt)
# 模型回答的依据来自检索到的真实资料,而非它脑子里可能过时或虚构的记忆——幻觉大幅下降
RAG 让我们的知识获取从"全靠模型预训练记忆、不知道也硬编、私有数据一无所知、幻觉满天飞"进化到了"先检索真实知识库拿到资料、再让模型基于资料作答、有据可循幻觉锐减":过去我们直接把用户问题丢给模型,指望它用预训练时'背'下来的知识回答,可这知识不仅有截止日期、对我们私有的业务数据(退款政策、产品细节、内部流程)更是一无所知,而模型最危险的特性是它不知道答案时绝不会老实说'不知道'、而是充满自信地编出一个看起来煞有介事的假答案,这种幻觉在客服问答场景里会直接误导用户、酿成投诉甚至事故;现在我们用 RAG,事先把私有知识库切成小块、用 embedding 转成向量存进向量数据库,用户一提问就先把问题也向量化、去向量库里按语义相似度检索出最相关的几段真实资料、再用重排模型精排一遍,然后把这些'确实存在于我们知识库里的真实资料'连同问题一起喂给模型、明确约束它只能基于给定资料回答、资料里没有就如实说不知道,模型回答的依据从'它脑子里可能过时或虚构的记忆'变成了'我们检索出来的真实资料',幻觉因此大幅下降、答案也能引用来源可供核查。我们的纪律是"涉及私有知识或事实准确性的问答一律走 RAG、prompt 里强约束只基于检索资料作答、知识库更新即时同步到向量库、关键场景给答案附上引用来源"。RAG 的本质认知是:大模型是一个强大的'语言推理引擎'但不是一个可靠的'知识数据库'——它擅长理解和组织语言,却会把记不清的知识自信地编造出来;RAG 的智慧在于把'知识'和'推理'解耦,让权威、可更新、可溯源的外部知识库负责'提供事实',让模型只负责'基于这些事实做理解和表达',用'检索到的真实资料'给模型的自由发挥套上事实的缰绳,这是让 LLM 在严肃场景下变得可信、可用的根本性手段。
三、结构化输出:从正则硬抠 JSON 到 function calling
第三仗,是让模型的输出可靠地被程序消费。古早时代我们让模型返回 JSON,就是在 prompt 里写一句"请返回 JSON 格式",然后用正则去抠它吐出来的那一坨文本——可模型时不时给你多包一层 markdown 的代码围栏、多写几句"好的,这是您要的 JSON:"、或者少个引号多个逗号导致 JSON 不合法,我们的解析代码三天两头崩,只能写一堆脆弱的容错去修补。现代做法是用结构化输出(function calling / JSON schema):你给模型一个明确的输出 schema,模型被约束着只能按这个结构生成、平台层保证返回的就是合法的结构化数据。下面是结构化输出的对比:
# 重构前:prompt 里求模型返回 JSON,再正则硬抠——模型多包代码围栏/多句废话就解析崩
# text = client.chat("提取信息返回JSON:" + user_text)
# json_str = re.search(r"\{.*\}", text, re.S).group() # 脆弱:多个逗号/少个引号就炸
# data = json.loads(json_str) # 三天两头 JSONDecodeError
# 重构后:用 schema 约束结构化输出,平台保证返回合法结构化数据,无需正则硬抠
from pydantic import BaseModel
class ExtractedOrder(BaseModel):
order_id: str
amount: float
status: str # 字段、类型都明确声明,模型被约束按此结构生成
# 把 schema 作为 response_format 传给模型,它只能产出符合 schema 的结构
resp = client.chat(
messages=[{"role": "user", "content": user_text}],
response_format=ExtractedOrder, # 结构化输出:平台保证产出符合 schema
)
order = resp.parsed # 直接拿到强类型对象,无需正则、无需手动 json.loads
print(order.order_id, order.amount) # 类型安全,字段缺失/类型错会在解析层被挡下
结构化输出让我们的输出消费从"prompt 求 JSON 再正则硬抠、模型多包围栏多句废话就解析崩、写一堆脆弱容错"进化到了"用 schema 约束模型按结构生成、平台保证合法结构化数据、直接拿强类型对象":过去要让模型返回能被程序处理的 JSON,我们只能在 prompt 里恳求它'请返回 JSON 格式',然后对它吐出来的自由文本用正则去硬抠那段 JSON,可模型的输出充满了不确定性——它时而把 JSON 包在 markdown 代码围栏里、时而在前面加一句'好的,这是您要的结果'、时而漏个引号多个逗号生成出不合法的 JSON,我们的解析代码就这么三天两头地崩,只好不断往上堆各种脆弱的字符串清洗和容错逻辑、还是防不胜防;现在我们改用结构化输出,用 Pydantic 之类定义一个字段和类型都明确的 schema、把它作为 response_format 传给模型,模型在生成时就被约束着只能产出符合这个 schema 的结构化数据、平台层保证返回的是合法结构,我们的代码直接拿到一个强类型对象、字段访问类型安全,再也不用写正则、不用手动 json.loads、不用为模型的格式自由发挥擦屁股。我们的纪律是"凡是输出要被程序消费的场景一律用结构化输出/function calling、用 schema 显式声明字段与类型、绝不再靠正则去解析自由文本、关键字段在解析层做校验"。结构化输出的本质认知是:让程序去解析为人类阅读而生成的自由文本,本身就是一件脆弱而拧巴的事——模型擅长生成流畅的自然语言,但自然语言的随意性正是程序消费它时一切解析崩溃的根源;结构化输出的智慧是把'约束'从软绵绵的 prompt 请求提升为硬性的 schema 契约,让模型在生成的源头就被约束在确定的结构里、而不是事后由我们去脆弱地修补它的自由发挥,这是连接'概率性的模型'与'确定性的程序'之间那道鸿沟的关键桥梁。
四、流式输出:从整段干等到 SSE streaming
第四仗,是响应的体验。古早时代我们调模型是同步阻塞的:用户问一句,我们把请求发出去,然后干等模型把整段几百字的回答全部生成完、一次性返回,这期间用户对着一个转圈的图标傻等十几二十秒、完全不知道是卡死了还是在思考,体验极差、跳出率很高。现代做法是流式输出(streaming):模型每生成一个 token 就立刻通过 SSE(Server-Sent Events)推给前端、像打字机一样一个字一个字地往外蹦,用户在第一个字出现的几百毫秒内就看到了响应、能边生成边读。下面是流式输出的实现:
# 重构前:同步阻塞,等模型把整段几百字全生成完才一次性返回,用户对着转圈干等 20 秒
# answer = client.chat(prompt) # 阻塞到全部生成完,首字延迟 = 总生成时间
# return answer
# 重构后:流式输出,模型每生成一个 token 就立刻 SSE 推给前端,像打字机一样即时可见
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
@app.post("/chat")
def chat(prompt: str):
def token_stream():
# stream=True:模型边生成边返回 token,不必等全部生成完
for chunk in client.chat(prompt, stream=True):
token = chunk.delta.content
if token:
yield f"data: {token}\n\n" # SSE 格式逐个 token 推送
yield "data: [DONE]\n\n"
# 首字延迟从"总生成时间"降到"生成第一个 token 的时间"(几百毫秒)
return StreamingResponse(token_stream(), media_type="text/event-stream")
流式输出让我们的响应体验从"同步阻塞、等整段几百字全生成完才返回、用户对着转圈干等十几二十秒"进化到了"逐 token 流式推送、首字几百毫秒即现、像打字机一样边生成边读":过去我们调模型是一锤子的同步阻塞调用,把请求发出去后就要一直等到模型把整段动辄几百字的回答完完整整生成完毕、才一次性把这一大段返回给用户,这意味着用户的等待时间等于整段内容的总生成时间、往往是十几到二十几秒,而这十几秒里用户面对的只是一个孤零零转圈的加载图标、完全不知道系统是死了还是在认真思考,焦虑感拉满、很多人没等到结果就不耐烦地走了;现在我们改用流式输出,模型采用 stream 模式边生成边吐 token、我们的服务用 SSE 协议把这些 token 一个接一个实时地推送给前端、前端像打字机一样一个字一个字地即时渲染出来,用户在请求发出后几百毫秒、第一个字蹦出来的那一刻就确切地知道'系统在正常响应了'、并且可以一边等模型继续生成一边阅读已经出来的内容,首字延迟从'整段总生成时间'骤降到'生成第一个 token 的时间'、感知上的等待被极大地缩短了。我们的纪律是"凡是面向用户的生成式交互一律流式输出、首字延迟作为核心体验指标盯死、流式过程中也要能优雅处理中断和错误、配合前端做好打字机渲染"。流式输出的本质认知是:对于一个需要较长时间才能产出完整结果的过程,'让用户感知到进展'和'缩短实际耗时'同样重要——同步阻塞最大的体验问题不在于慢,而在于那段时间里用户对系统状态一无所知的焦虑黑箱;流式输出的智慧是把一个'要等很久的整体结果'拆成'一串立即开始、持续可见的增量',用户不必等到全部完成就能看到系统在实实在在地工作、并提前开始消费已产出的部分,这是把生成式 AI 那不可避免的生成耗时,转化为可接受甚至流畅体验的关键。
五、韧性与网关:从裸调 API 到 LLM 网关统一兜底
第五仗,是调用的韧性与治理。古早时代我们的代码里到处直接 import 某家模型的 SDK、硬编码模型名、裸调它的 API——没有超时(模型偶尔卡住就把请求挂死)、没有重试(网络抖一下或触发限流就直接失败)、没有降级(这家服务挂了整个功能就废了)、想换个模型或多模型对比要改一大堆散落各处的调用代码。现代做法是在应用和各家模型之间架一层 LLM 网关:统一接入所有模型、统一加上超时重试降级、按需把请求路由到不同模型(便宜的模型扛简单任务、强模型扛难任务)、统一做限流和成本控制。下面是 LLM 网关的核心能力:
# 重构前:到处直接裸调某家 SDK,硬编码模型名,无超时无重试无降级,换模型要改一堆代码
# resp = openai_sdk.chat(model="gpt-x", messages=msgs) # 卡住挂死、限流就失败、挂了就废
# 重构后:统一走 LLM 网关——统一接入 + 超时重试降级 + 多模型路由 + 成本控制
class LLMGateway:
def chat(self, messages, task_tier="standard"):
# 多模型路由:简单任务走便宜快的模型,难任务才上强模型,成本与效果平衡
primary, fallback = self.route(task_tier)
for attempt in range(3): # 重试:应对偶发抖动和限流
try:
return self._call(primary, messages, timeout=20) # 超时:不让请求挂死
except (RateLimitError, TimeoutError):
time.sleep(2 ** attempt) # 指数退避
except ServiceError:
break
# 降级:主模型不可用时自动切到备用模型/备用厂商,功能不至于整个废掉
return self._call(fallback, messages, timeout=20)
def route(self, tier):
return {"cheap": ("small-model", "small-backup"),
"standard": ("mid-model", "small-model"),
"hard": ("strong-model", "mid-model")}[tier]
# 应用层只跟网关打交道:换模型、加厂商、调路由策略都在网关一处改,业务代码无感
LLM 网关让我们的调用从"到处裸调某家 SDK、硬编码模型、无超时重试降级、换模型改一堆代码"进化到了"统一网关接入、超时重试降级兜底、多模型按需路由、成本与效果可调":过去我们的业务代码里到处直接 import 某一家模型厂商的 SDK、把模型名硬编码在调用处、然后就这么裸调它的 API——没有超时控制,模型偶尔生成卡住一个请求就被无限期挂死;没有重试,网络稍微抖一下或者触发了厂商的限流就直接报错失败;没有降级,一旦这家厂商的服务出故障、我们依赖它的整个 AI 功能就跟着全废;想换个模型、想接入第二家厂商做对比或冗余、想让简单任务走便宜模型省钱,都得去翻遍代码里散落各处的调用点一个个改,牵一发动全身;现在我们在应用和各家模型之间架了一层统一的 LLM 网关,所有模型调用都只跟网关打交道,网关统一加上了超时(不让请求挂死)、重试加指数退避(扛住偶发抖动和限流)、降级(主模型挂了自动切备用模型或备用厂商、功能不至于整个瘫痪),还能按任务难度把请求路由到不同档位的模型(简单活儿走便宜快的小模型、难活儿才上贵的强模型)、统一做限流和成本核算,换模型、加厂商、调路由策略全在网关这一处改、业务代码完全无感。我们的纪律是"严禁业务代码直接裸调模型 SDK、一律走 LLM 网关、任何模型调用必设超时、对限流和抖动配重试退避、关键功能必配降级备用模型"。韧性与网关的本质认知是:外部模型服务和任何外部依赖一样会卡顿、会限流、会宕机,把对它的调用裸露在业务代码各处、且不加任何兜底,就是把整个应用的稳定性和灵活性都抵押给了一家随时可能出问题的外部服务;LLM 网关的智慧是用一层统一的抽象把'易变、不可靠的外部模型'与'稳定的业务逻辑'隔离开,在这一层集中解决韧性(超时重试降级)、治理(路由限流成本)和灵活性(多模型可切换)的问题,让业务代码既不必关心底下是哪家模型、也不必为它的不可靠买单,这是严肃 LLM 应用必备的基础设施。
六、语义缓存:从重复 query 重复烧钱到语义命中复用
第六仗,是成本。古早时代每一次模型调用我们都老老实实地真的去调一次 API、付一次 token 的钱,哪怕是大量用户问的其实是同一个意思的问题(只是措辞不同)——"怎么退款""退款流程是啥""我想退货怎么办",这三个问题对模型而言答案几乎一样,我们却为它们各付了一次全价,大量本可避免的重复调用把账单烧得飞起。现代做法是语义缓存:不是像传统缓存那样要求 query 字符串完全相同才命中,而是把 query 转成向量、按语义相似度匹配——只要新问题和缓存里某个问题的语义足够接近,就直接返回缓存的答案、根本不调模型。语义缓存让我们的成本控制从"每次调用都真调真付费、大量语义相同的问题重复全价烧钱"进化到了"按语义相似度命中缓存、相近问题直接复用答案、重复调用大幅省钱":过去我们对模型的每一次调用都是实打实地请求一次 API、为这次生成的 token 付一次真金白银,可现实中海量用户问的问题在语义上高度重复——'怎么退款'和'退款流程是什么'和'我要退货怎么操作'本质上是同一个问题,模型给出的答案也几乎一模一样,但因为字符串不同、传统的精确匹配缓存完全命中不了,于是我们为这些语义重复的问题一遍又一遍地付着全价,token 账单高得吓人、其中一大半都浪费在了本可避免的重复生成上;现在我们上了语义缓存,把每个 query 转成向量、按语义相似度去匹配缓存,只要新来的问题和缓存里某个历史问题的语义足够接近(相似度超过阈值),就直接把缓存里的答案返回、压根不去调模型,只有语义上确实是新问题才真正请求模型并把结果存入缓存,大量语义重复的调用被缓存挡下、token 成本和响应延迟双双大幅下降。我们的纪律是"高频且答案稳定的问答场景上语义缓存、相似度阈值要调得保守避免误命中答错、对时效性强或个性化的 query 不缓存、缓存答案也要能随知识更新失效"。语义缓存的本质认知是:LLM 调用是按 token 真金白银计费的昂贵操作,而用户的提问在语义层面存在大量重复——传统缓存只认'字符串完全相同'、对这种'意思相同但说法不同'的重复无能为力;语义缓存的智慧是把缓存的匹配标准从'字面相等'升级到'语义相近',用向量相似度去识别那些'换了个说法的同一个问题',从而把昂贵的重复生成转化为近乎免费的缓存命中,这是 LLM 应用在规模化后控制成本的一把利器,但用它的功力全在于把相似度阈值拿捏得恰到好处、既要省钱又绝不能为了省钱而返回似是而非的错答案。
七、自动化评估:从人肉看效果到 eval 与 LLM-as-judge
第七仗,也是把 LLM 应用从玄学拉回工程的关键一仗,是评估。古早时代我们改 prompt、换模型、调 RAG,改完效果到底是好了还是坏了?全靠人肉抽几个例子看看、凭感觉拍脑袋说'好像变好了',完全没有客观标准,经常是改了 A 场景变好、却不知道悄悄把 B 场景改坏了,上线后才被用户投诉发现。现代做法是建立自动化评估:沉淀一个覆盖各种场景的评测数据集(问题 + 期望答案/评判标准),每次改动都跑一遍这个数据集、用指标(准确率、相关性等)定量打分,对于开放式的回答质量则用 LLM-as-judge(让一个强模型按评分标准给答案打分)来自动评判。自动化评估让我们的效果衡量从"人肉抽几个例子看、凭感觉拍脑袋说好像变好了、改好 A 却悄悄改坏 B"进化到了"跑评测数据集定量打分、LLM-as-judge 自动评判、改动好坏一目了然":过去我们每次动 prompt、换模型、调 RAG 参数,改完之后效果究竟是变好还是变坏,判断方式就是随手抽几个例子人工看一眼、然后凭主观感觉下个'嗯好像是好点了'的结论,既没有客观的衡量标准、也覆盖不全各种场景,结果常常是我们为了优化 A 类问题改了 prompt、A 确实变好了、却完全没察觉这个改动把 B 类问题悄悄改坏了,直到上线后用户投诉才发现按下葫芦浮起瓢;现在我们建起了自动化评估体系,沉淀了一个覆盖各类真实场景的评测数据集、每条都带着问题和期望答案或评判标准,每次任何改动都自动在整个数据集上跑一遍、用准确率相关性等指标定量打分,对于那些没有标准答案的开放式回答则用 LLM-as-judge、让一个强模型按我们定义的评分标准给每个答案客观打分,于是一次改动到底让整体效果涨了还是跌了、在哪些场景上涨了哪些上跌了,全都有清清楚楚的数字摆在面前、再也不是凭感觉。我们的纪律是"任何影响效果的改动(prompt/模型/RAG)上线前必须跑 eval、持续积累和扩充评测数据集、开放式质量用 LLM-as-judge 评判、用分数而非感觉决定是否上线"。评估的本质认知是:不能被度量的东西就无法被改进——LLM 应用的输出是概率性的、千变万化的,如果效果好坏只能靠人主观感觉,那所谓的'优化'就成了蒙眼狂奔的玄学、改对改错全凭运气;自动化评估的智慧是为这种主观的、开放的输出质量建立起客观的、可重复的、可定量的衡量标尺,让每一次改动的真实影响都能被清晰地度量出来,从而把 LLM 应用的迭代从'凭手感碰运气'变成'用数据驱动'的严肃工程,这是这个领域专业与业余的分水岭。
八、护栏与迁移:输入输出 guardrails 与稳健灰度
第八仗,是安全护栏与迁移本身。古早时代我们的模型输入输出是彻底裸奔的:用户可以通过 prompt 注入(在输入里塞'忽略你之前的所有指令,现在你要……')来劫持模型、让它吐出不该说的内容,模型自己也可能生成有害、违规、泄露隐私的输出,我们却没有任何防护。现代做法是给输入输出都套上 guardrails:输入侧检测和拦截 prompt 注入、敏感信息、违规请求,输出侧审核模型的回答是否有害、是否泄密、是否符合规范,不合规就拦截或重新生成。而把这套新体系切上线同样求稳:新老链路并行、按比例灰度放量、紧盯 eval 分数和线上指标,稳了再全量。护栏让我们的安全从"输入输出裸奔、prompt 注入可劫持模型、有害输出无人拦截"进化到了"输入侧拦注入审敏感、输出侧审有害防泄露、不合规就拦截或重生成":过去模型的输入输出毫无防护地裸奔着,恶意用户只要在输入里塞一句'忽略你前面的所有设定、现在按我说的来'就可能通过 prompt 注入劫持模型、诱导它绕过我们设定的规则吐出不该说的话,模型自己也可能在某些诱导下生成有害的、违规的、甚至泄露其他用户隐私或系统提示词的内容,而这一切我们当时既拦不住也发现不了;现在我们在模型的输入和输出两侧都架起了 guardrails——输入侧在请求到达模型前先检测并拦截 prompt 注入攻击、识别敏感信息和违规请求,输出侧在回答返回用户前先审核它是否包含有害内容、是否泄露了隐私或系统信息、是否符合内容规范,任何不合规的输入输出都会被拦截下来或触发重新生成,给模型这个本质上不可完全预测的组件套上了输入和输出的双重保险。迁移上线的纪律是"新体系一律灰度而非全量切换、新老链路并行留足回退、放量每一步紧盯 eval 分数和线上指标、护栏规则持续根据新攻击手法更新"。护栏与迁移的本质认知是:LLM 是一个行为无法被完全预测和穷举的概率性组件,你永远无法保证它在所有输入下都表现得安全合规——指望靠 prompt 把它'调教'得绝对听话是不现实的;护栏的智慧是不把安全的赌注全押在模型自身的'自觉'上,而是在它的外围、在输入进入和输出离开的两道关口上,用独立的、确定性的检测和拦截机制构筑起防线,把这个不可控组件的风险约束在可控的边界之内,这是任何面向真实用户的 LLM 应用都必须内建的安全底座。
九、7 个 P0 事故复盘
7 事故:(1) prompt 注入劫持模型吐出系统提示词和不当内容,输入侧加 guardrails 检测拦截注入;(2) 模型幻觉编造了不存在的退款政策误导用户引发投诉,关键问答全面切 RAG 并强约束只基于资料作答;(3) 正则抠 JSON 在格式微变时大面积解析失败致功能瘫痪,全面改结构化输出 function calling;(4) 主模型厂商故障导致整个 AI 功能全废,LLM 网关补齐降级到备用模型;(5) 一次 prompt 改动优化了 A 场景却悄悄改坏 B 场景上线才发现,任何改动上线前强制跑 eval;(6) 没有成本监控某 bug 导致死循环调用一夜烧掉巨额 token,接入 token/成本可观测 + 限流 + 语义缓存;(7) 超长上下文被无声截断导致关键信息丢失答非所问,加上下文窗口管理与压缩、超限显式告警。每个 P0 都做 5-Why 复盘,固化成上线评估门禁、护栏规则库或成本告警基线,确保同类问题不再复发。
十、AI 工程师的 6 条工程哲学
6 哲学:(1) prompt 是源代码——必须模板化、版本化、可评估,绝不当随手字符串散落;(2) 模型擅推理而非记知识——事实交给 RAG 检索,别让模型凭记忆编;(3) 不能度量就无法改进——用 eval 和数据驱动迭代,绝不靠感觉拍脑袋;(4) 模型是概率性的,系统要确定性——用结构化输出和护栏给不确定的模型套上确定的约束;(5) 模型会失败要兜底——超时重试降级护栏一个都不能少,别把稳定性押给外部模型;(6) 每个 token 都是钱——用缓存、路由、限流把成本当一等指标来管。这 6 条哲学,是我们用 7 个 P0 事故和 87 天攻坚换来的集体共识。它们共同指向一个认知:LLM 应用现代化的价值不在于"用了多强的模型"这个动作本身,而在于把"AI 应用的准、稳、省、安全"从依赖模型恰好不抽风和工程师恰好调对了 prompt 的运气,前移成了由工程机制(prompt 工程化、RAG、结构化输出、eval、护栏、网关)结构性保障——会做现代 AI 工程的团队,是在用机制把一整类"幻觉、注入、解析崩溃、成本失控、效果倒退、厂商故障"的问题从源头消除,而不只是在事后救火。
十一、重构收益的量化:7 个关键数字
7 数字:(1) 答案幻觉率:模型凭记忆瞎编频发 → RAG 检索增强后锐减;(2) 输出解析失败率:正则抠 JSON 三天两头崩 → 结构化输出后归零;(3) 首字响应延迟:整段干等十几秒 → 流式输出后降到几百毫秒;(4) token 成本:重复 query 重复全价 → 语义缓存 + 多模型路由后大幅下降;(5) 效果回归发现:改坏了上线才知道 → eval 门禁后上线前就拦住;(6) 厂商故障导致的功能不可用:主模型挂就全废 → 网关降级后近乎无感;(7) prompt 注入与有害输出:裸奔无防护 → 输入输出护栏后被有效拦截。这些数字背后,是 87 天里 13 个人无数次的 prompt 打磨、RAG 调优、评测集积累、护栏规则编写和网关建设,但每一个都实打实地转化成了准确性、体验、成本和安全性的提升。当我们把这份数据汇报给管理层时,最有说服力的不是任何 AI 名词,而是"模型不再一本正经地胡编误导用户、AI 功能不再因为一家厂商抖动就整个瘫痪"这两条。
十二、留给后来者的最后一句话
87 天的 LLM 应用现代化战役,我们走过的不只是一条从拼 prompt 到模板化、从硬记到 RAG、从正则抠 JSON 到结构化输出、从干等到流式、从裸调到网关、从重复烧钱到语义缓存、从人肉看到自动化 eval、从裸奔到护栏的技术升级路,更是一次从"靠模型恰好不抽风、靠工程师恰好把 prompt 调对了的运气"到"靠工程机制和评估护栏结构性兜底"的开发范式跃迁。当模型不再一本正经地编造不存在的政策误导用户、当输出格式不再三天两头解析崩溃、当用户在几百毫秒内就看到答案像打字机一样流出、当一家模型厂商半夜抖动我们的功能却因为网关降级而稳如泰山、当一次 prompt 改动的好坏在上线前就被 eval 清清楚楚地量化出来、当 prompt 注入和有害输出被护栏稳稳拦在门外的那一刻,真正点燃我们的,不是用了多强的模型本身,而是"AI 应用的准、稳、省和安全,终于从依赖模型和手感的运气,变成了由机制和评估强制保障"的踏实与笃定。LLM 应用现代化没有银弹,关键是理解 prompt 工程化、RAG、结构化输出、流式、网关、缓存、评估、护栏各自解决什么问题、又各自带来什么代价,然后从治理幻觉和建立评估的地基起步、用灰度可回退的方式落地——尤其要克制"图省事拼个 prompt 就上、图省事正则抠个 JSON、图省事裸调一家模型不做降级、图省事不写 eval 凭感觉发布"的旧习惯,因为每一个散落的 prompt、每一段脆弱的正则、每一次没有兜底的裸调、每一回凭感觉的上线,都是在亲手埋下未来某次幻觉误导用户的投诉或某个深夜成本失控的告警。愿每一位还在和幻觉、注入、解析崩溃、成本失控搏斗的同行,都能早日让自己的 AI 应用被工程机制稳稳地守护。共勉,后会有期。
—— 别看了 · 2026