Chapter 04

工程化与全栈落地:把 demo 变成生产系统

上一章造出了能自主决策的 Agent——代价是成本、延迟、失控三笔账同时找上门。这一章是把 demo 变成生产系统要补的工程:让响应可感知(流式)、让账单可控(成本)、让黑盒可查(可观测)、让质量不回退(评估)、让它不被劫持(安全)、让它不雪崩(可靠性),最后落到框架怎么选。

本章你要建立的心智模型

  • 流式为什么是一等公民,SSE 与 WebSocket 怎么选,TTFT 买的是哪种延迟
  • token 成本的几个杠杆(缓存 / 路由 / 批处理)按什么顺序拉,谁先动收益最大
  • 怎么给一个非确定的多步系统做可观测与评估——trace/span 与 offline/online 各管什么
  • 生产里最常见的不是答错,而是重试风暴——可靠性与安全(注入、致命三件套)怎么做

判断一个候选人是不是真上过生产,看一道题就够:「你的大模型应用上线后,最常见的线上事故是什么?」初级答「答错 / 幻觉」,中高级答「重试风暴把上下文二次撑大,是一笔成本和延迟的账,不是质量的账」。这一章把工程化拆成七块,每块都落到一个能在白板上算清的取舍。

4.1流式输出:SSE vs WebSocket 与 TTFT

流式不缩短总耗时,只把「首 token 时间(TTFT)」提前——它买的是感知延迟,不是真实延迟。

为什么需要它

LLM 逐 token 生成,一段几百字的回答端到端要数秒。非流式接口在全部生成完之前不返回任何字节,用户盯着空白等 5–10 秒——这是体感上最劝退的延迟。流式把「已经生成的 token」边产边推,用户在几百毫秒内就看到第一个字,后面持续滚动。总耗时一字不少,但等待从「白屏焦虑」变成「正在打字」。这就是为什么 OpenAI、Anthropic、Vercel AI SDK 把流式做成默认行为,而不是可选项。

TTFT(Time To First Token)= 分词 + prefill(把 prompt 过一遍算 KV cache)+ 排队等待。它是流式系统的核心指标:用户感知的「快慢」基本由 TTFT 决定,而非总时长。多步 Agent 里延迟会线性累加——每次工具调用 +300ms,串行八步就是 2–4 秒纯编排开销,叠加每步的 LLM 生成,不做流式的多步 Agent 在用户眼里就是「卡死了」。

浏览器 后端 LLM HTTP POST 提问 开流 stream=true token #1 SSE data: #1 ← TTFT 到此 token #2 SSE data: #2 …逐个推送… token #N SSE data:[DONE] 对比 · 非流式 浏览器全程白屏,等 token #N 生成完才一次性收到整段 TTFT ≈ 总耗时(最差体感)
图 4.1token 在 LLM→后端→浏览器之间逐个回流,首个 token 一到(朱红那条)用户就开始看到内容。注意:流式不改总耗时,只把 TTFT 提前——它买的是感知延迟,不是真实延迟;非流式(底部)的 TTFT 等于总耗时,所以体感最差。

SSE 与 WebSocket 怎么选。SSE(Server-Sent Events)是纯 HTTP、单向(只有 server→client 推 token),天然走现有的 HTTP 基础设施,代理 / CDN / 鉴权全部复用,浏览器原生 EventSource 自动重连。代价是只能服务端单向推:用户想「中途打断生成」得另开一个 HTTP 请求去取消。WebSocket 是全双工长连接,适合需要在生成途中双向通信的场景——边生成边接受用户中断、人工审批插入、多 Agent 实时转向。代价是它脱离 HTTP 语义,负载均衡、鉴权、断线重连都要自己处理,运维复杂度高一档。

表 4.1 · 流式传输方式选型
方案优势代价 / 为什么不默认选它
SSE(默认)纯 HTTP 单向、代理/CDN/鉴权全复用、浏览器原生自动重连、实现最简仅 server→client 单向;「停止生成」要另开请求;记得关 Nginx proxy_buffering 否则 token 被缓冲到一起
WebSocket全双工,可在生成途中中断 / 插入审批 / 实时转向脱离 HTTP 语义,LB / 鉴权 / 重连全自理,运维复杂度 +1 档;多数对话场景用不上双向
轮询 polling实现无脑、任何栈都能跑延迟高、请求数爆炸、体验差;只在无法长连接的受限环境用
陷阱 · HTTP 200 之后才出错

流式响应一旦发出 200 OK 和第一个字节,HTTP 状态码就定死了。生成到一半 LLM 报错、超时、触发内容过滤,无法再改 HTTP 状态码——只能在流内发一个自定义错误事件(如 event: error\ndata: {...}),前端解析流内容来识别失败。把错误当成普通 HTTP 异常处理,前端会以为请求成功了却收到半截答案。

sse_stream.py(演示用,未本机执行) Python
# FastAPI 服务端 SSE 流式 —— 演示用,未本机执行
from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()

async def token_stream(prompt: str):
    usage = {"completion_tokens": 0}
    try:
        async for chunk in llm.astream(prompt):   # 上游逐 token 产出
            usage["completion_tokens"] += 1
            # SSE 帧格式:data: <payload>\n\n
            yield f"data: {chunk.text}\n\n"
    except Exception as e:
        # 200 已发出,状态码改不了 —— 只能走流内错误事件
        yield f"event: error\ndata: {str(e)}\n\n"
    finally:
        # 结束帧顺带回传 token 数,供成本统计
        yield f"event: done\ndata: {usage['completion_tokens']}\n\n"

@app.post("/chat")
async def chat(prompt: str):
    return StreamingResponse(
        token_stream(prompt),
        media_type="text/event-stream",
        headers={"X-Accel-Buffering": "no"},  # 关掉 Nginx 缓冲,否则 token 会被缓冲攒成一大块
    )
token 统计流式下 usage 不在响应头里,要边推边累加(或读结束帧)。 X-Accel-Buffering这一行是 SSE 在 Nginx 后最常见的失败修复点。
想一想

SSE 部署在 Nginx 反向代理后面,前端却要等好几秒才一次性收到全部文字,「流式」完全失效。问题最先该查哪里?

展开答案(先停 10 秒再点)

Nginx 默认开启 proxy_buffering on,会把上游的 SSE 数据攒满一个缓冲区再下发,于是逐 token 推送被重新缓冲、合并成一大块再一次性下发。需要在 location 上设 proxy_buffering off;,或让后端发 X-Accel-Buffering: no 响应头。

设计启示:流式是一条贯穿浏览器→CDN→反代→应用的端到端管道,任何一环开了缓冲都会让流式退化成非流式。这就是为什么流式要「一开始就设计进架构」,而不是上线后再加——它牵扯的是整条链路的配置。

4.2成本优化:缓存 / 路由 / 批处理

省钱的杠杆有优先级:先用模型路由砍掉「大材小用」,再用 prompt 缓存吃掉重复前缀,最后用批处理收割可异步的流量。

为什么需要它

Agent 一次 turn 扇出 6–12 次 LLM 调用,每次都把完整工具定义 + 系统提示 + 历史重新发一遍。token 成本随轮次和上下文长度线性甚至超线性增长,一个月的账单很容易从「能忽略」涨到「老板要问」。成本优化不是省小钱,而是决定这个应用商业上能不能跑通。关键是杠杆有顺序——拉错顺序会做无用功。

四个杠杆,按收益和改动成本排序。① 模型路由 / 级联:简单任务(分类、抽取、改写)走小模型,难任务才升级到大模型,按置信度分流——据厂商数据可省下可观成本(实际取决于任务分布与降级阈值),这是收益最大、最该先做的杠杆,因为它直接砍掉「大材小用」的结构性浪费。② prompt 前缀缓存:把静态前缀(工具定义→系统提示→固定上下文)缓存住,Anthropic 的缓存读取只算 0.1× 输入价(约 9 折,即省 90%),写入 1.25×(5 分钟 TTL)或 2×(1 小时 TTL);几乎零质量损失,改动也小,是性价比最高的第二步。③ 批处理:OpenAI Batch API 固定5 折、异步 ≤24h 返回、不支持流式,适合离线评估 / 批量摘要这类非实时流量;可与缓存叠加,组合省到约 75%。④ 语义缓存:用 embedding 相似度命中历史问答,命中率约 25–35%,但有「相似但不相同」答错的风险,要配阈值和兜底,放最后。

① ② ③ ④ 模型路由 / 级联 简单任务走小模型,按置信度升级 收益最大 · 砍结构性浪费 · 省下可观成本 Prompt 前缀缓存 缓存静态前缀(工具→系统→上下文) 读取 0.1× ≈ 9 折 · 几乎零质量损失 批处理 Batch 非实时流量异步提交 固定 5 折 · ≤24h · 不支持流式 · 可叠加缓存→约省 75% 语义缓存 embedding 相似命中历史问答 命中率 25–35% · 有「相似≠相同」答错风险 · 放最后兜底 越靠上 → 改动越小、收益越确定;越靠下 → 越要配阈值兜底
图 4.2四个成本杠杆按「改动成本低 + 收益确定」从上往下排。注意:先做模型路由(砍结构性浪费)和 prompt 缓存(几乎无损),把语义缓存这类「会答错」的杠杆放到最后——拉杠杆的顺序错了,要么白做功,要么先伤了质量。
表 4.2 · 成本杠杆的拉动顺序
杠杆省多少 / 怎么省代价
① 模型路由 / 级联可省下可观成本(实际看任务分布与降级阈值);简单任务下沉小模型,低置信再升级要建路由逻辑 + 置信度判断;误判会把难题丢给小模型答错
② Prompt 前缀缓存命中读取 0.1×(9 折);缓存静态前缀写入贵 1.25×/2×、有 TTL;前缀一变(哪怕一个字)缓存全失效,故要把变动内容放末尾
③ 批处理 Batch固定 5 折,可叠加缓存到约 75%异步 ≤24h、不支持流式;只适合离线 / 非实时流量
④ 语义缓存命中率 25–35%,命中即零成本「相似但不同」会答错;需相似度阈值 + 兜底回源
洞察 · 缓存的字节顺序就是省钱的关键

缓存命中省的不是一次网络往返,而是 prefill 阶段那段前缀的 KV(注意力 key/value)状态的重算(见 §1.1)——服务端把前缀算好的 KV 状态存下,命中时直接复用、跳过重算。这也解释了它的匹配规则:前缀按「从开头逐字节匹配」生效,一旦某个 token 变了,从那个 token 起 KV 状态就和缓存分叉,后面全部失效。所以 prompt 的拼接顺序必须是「最稳定的放最前,最易变的放最后」:工具定义 → 系统提示 → 长期上下文 → 本轮用户输入。把时间戳、随机 ID、用户名这类每次都变的内容塞在前缀里,等于亲手让缓存命中率归零。

想一想

有人为了「让缓存多命中」,每次请求都把整段对话上下文原样缓存一遍。延迟和成本会变好还是变差?

展开答案(先停 10 秒再点)

多半变差。缓存写入比普通输入贵(1.25× 或 2×),而对话上下文每轮都在增长、内容每轮都在变——前缀里只要有变动,缓存就命中不上,等于每轮都在「付高价写入 + 零命中读取」。缓存只对稳定不变的前缀(工具定义、系统提示)才划算;把易变的整段历史拿去缓存,是花写入的钱买不到读取的折扣。

设计启示:缓存收益 = 命中率 × 读取折扣 − 写入溢价。命中率由「前缀稳定性」决定,不是「缓存得越多越好」。

4.3可观测性:trace 与 span

没有 trace,你回答不了「哪一步失败 / 哪一步变贵」——而这正是 LLM 应用排障的全部。

为什么需要它

LLM 应用是非确定的,同样的输入两次跑出不同路径;一次用户「turn」会扇出 6–12 次 LLM / 工具调用。出了问题——变慢、变贵、答错——光看一条请求日志根本定位不到是哪一跳出的事。可观测性把一次 turn 还原成一棵可下钻的调用树,让「哪一步」变成可回答的问题。这是把黑盒变成可运维系统的前提。

trace 与 span 的关系。一次完整 turn = 一个 trace;trace 里每一个工作单元(一次检索、一次 LLM 调用、一次工具执行)= 一个 span;span 之间有父子嵌套,组成调用树。生成型 span(LLM 调用)必须记全:prompt、completion、模型 + 参数、token 数、成本、延迟——缺任何一项都会在排障时卡住。落地工具上,Langfuse(基于 OpenTelemetry、可自托管,数据合规友好)和 LangSmith(P50/P99 延迟、成本、错误率看板)是社招里最常被点名的两个。

TRACE · 一次用户 turn 总延迟 4.2s · 总成本 $0.031 span · 检索 retrieval 120ms · top-k=5 span · LLM 规划(生成型) 980ms · 1.2k tok · $0.018 span · 工具调用 search_api 2100ms ← 最慢一跳 span · LLM 作答(生成型) 1000ms · 0.9k tok · $0.013 瓶颈在这一跳
图 4.3一次 turn 是一个 trace,下挂检索 / LLM / 工具等子 span,每个生成型 span 都带 token、成本、延迟。注意:把这四项记在每个 span 上,「哪一跳变慢变贵」一眼可见(这里是工具调用的 2100ms)——没有这棵树,你只能猜。
表 4.3 · 可观测的三个层次(缺一不可)
层次记什么回答什么问题
链路追踪 trace/span每个 span 的 prompt / 输出 / 模型 / token / 成本 / 延迟哪一跳失败、变慢、变贵
指标 metricsP50/P99 延迟、错误率、token 燃烧率、缓存命中率整体健康度、趋势、告警
审计 audit谁在什么时间问了什么、触发了哪些工具合规、安全事件回溯
陷阱 · 效果会随时间衰减

上线那天评估通过 ≠ 永远没问题。模型供应商静默升级、用户提问分布漂移、知识库更新,都会让效果悄悄下滑。可观测性要带上持续监控:把线上输出按采样喂给 online 评估(见 4.4),盯住质量指标的趋势线,而不是只在上线前跑一次。「上线后效果衰减怎么监控」是这道题的高频追问。

4.4评估:offline / online 与 LLM-as-judge

offline 评估是上线前的 gate(防回归),online 评估是上线后的雷达(抓衰减)——两者管的是不同阶段,不可互相替代。

为什么需要它

Prompt 改一个字、换一个模型版本、调一处检索参数,任何一处都足以让某类问题悄悄变差。没有评估,你只能靠「线上有人投诉」来发现回归,那时已经晚了。评估把「质量」变成 CI 里能 gate、看板上能盯的数字。评 Agent 比评 LLM 难:路径不唯一、中间步骤难判对错,所以除了最终正确性,还要看过程指标(步数、工具调用准确率、成本)。

offline 与 online 分工。offline 跑在精心策划的 golden dataset 上,进 CI/CD,上线前当 gate——改动合并前先在固定测试集上回归,分数掉了就拦下。online 跑在生产真实流量上,用 canary / A-B 灰度,抓的是离线集覆盖不到的真实分布漂移和效果衰减。

LLM-as-judge 与它的偏见。用一个强模型给输出打分,可扩展、便宜,但有系统性偏见:位置偏见(成对比较时偏向某个固定位置的答案,实测偏好率高达 60–75%)、啰嗦偏好(倾向给更长的答案高分)、自我偏好(倾向给同家族模型的输出高分)。这些偏见是结构性的、不是 bug:judge 自己就是个没有标准答案可对照的自回归模型,给不出"真值",只能用训练里和"好答案"相关的表层特征(更长、排在前、同家族风格)去近似——这正是它系统性偏向长答案 / 前位 / 自家输出的根因。缓解手段:成对比较时交换 A/B 顺序跑两遍取一致结果(能压住位置偏见,却压不掉啰嗦 / 自我偏好),并先用人工标注校准 judge。RAGAS 这类框架给 RAG 提供 faithfulness、answer relevancy、context precision/recall 等现成指标。

表 4.4 · offline vs online 评估
维度offline 离线评估online 在线评估
数据来源策划的 golden dataset(固定)生产真实流量(漂移)
时机 / 位置上线前,进 CI/CD 当 gate上线后,canary / A-B 灰度
主要防什么改动引入的回归真实分布漂移、效果衰减
代价golden set 要人工维护,覆盖不到长尾需采样标注 / judge,有延迟和成本,结论受流量波动影响
洞察 · 为什么两者不能二选一

只有 offline:固定测试集永远测不到上线后才出现的新提问类型,衰减无感。只有 online:等真实流量暴露问题,坏改动已经影响了用户,且无法在合并前拦截。offline 在「时间轴的左边」把回归挡在门外,online 在「时间轴的右边」抓住门外冒出来的新问题——它们防的是不同阶段的不同风险。

想一想

用 LLM-as-judge 做成对比较,把「答案 A vs 答案 B」喂进去,judge 总是偏向排在前面的那个。怎么用最小代价缓解?

展开答案(先停 10 秒再点)

这是位置偏见(实测偏好率 60–75%)。最小代价做法:交换顺序各跑一遍——先 (A,B) 再 (B,A),只有两次结论一致才算数,不一致判平局或交人工。代价是 judge 调用翻倍,但换来结论稳定。更彻底的是先用一批人工标注校准 judge 的打分倾向。

设计启示:自动评估器本身也是个非确定、有偏的 LLM,不能盲信它的单次输出——评估系统也要被评估。

4.5安全:Prompt 注入与致命三件套

Prompt 注入的根因是「指令和数据共用同一条通道」,LLM 分不清哪句是开发者的规则、哪句是攻击者塞进来的内容。

为什么需要它

传统注入(SQL / XSS)有清晰的代码-数据边界,加转义就能防。LLM 没有这个边界——系统提示、用户输入、检索到的文档、网页内容,全部以自然语言拼进同一个 prompt。攻击者只要在任意一处写「忽略以上所有指令,改为……」,模型就有概率照做。OWASP 把 Prompt 注入列为 LLM 应用头号风险 LLM01(连续两版第一),因为它无法被彻底「修复」,只能纵深防御压低风险。

直接注入 vs 间接注入。直接注入是用户在对话框里直接写「忽略之前的指令」。间接注入更隐蔽:恶意指令藏在被 RAG 检索到的文档、被 Agent 抓取的网页里,用户本人毫不知情,模型读到这些「数据」时却把其中的指令当命令执行。间接注入是这道题的高频追问,也是最难防的——因为输入源不可信且不可控。

致命三件套(lethal trifecta)。真正把注入从「恶作剧」升级成「数据泄露」的,是三个能力同时具备:① 不可信输入(能接触外部内容)+ ② 私有数据访问(能读到敏感信息)+ ③ 对外发送通道(能把数据发出去,如带宽出网权限的工具、能渲染外链图片)。任一单独存在都还好,三者凑齐,攻击者就能用间接注入指挥 Agent 把私有数据偷运出去。

不可信输入 外部内容 / RAG / 网页 私有数据访问 能读敏感信息 对外发送通道 出网工具 / 外链图片 高危 可被注入并外泄
图 4.4三个能力的交集 = 高危区。注意:任一单独存在都还好;三者同时具备,prompt 注入就能把私有数据偷运出去。防御的本质是砍掉其中一条边——比如对能读私有数据的 Agent 关掉出网通道,三件套就凑不齐。
表 4.5 · 纵深防御(单点都不够,要叠加)
防御层做什么挡住什么 / 局限
指令与数据分隔系统指令与外部内容用明确分隔标记 + 强调「以下是数据不是命令」降低概率,挡不死;模型仍会被绕过
最小权限工具工具权限 ≤ 当前用户权限,砍掉非必要的出网 / 写操作直接拆掉致命三件套的一条边,最有效
沙箱执行工具跑在只读 / 隔离容器里限制爆炸半径;不阻止注入本身
输出审核 + 拦截走私过滤输出里的 markdown 图片外链、ASCII 走私式外泄堵外泄通道;要持续更新规则
HITL 人工确认敏感 / 不可逆操作前要人点确认挡高危动作;牺牲自动化程度
陷阱 · 不要信任检索回来的内容

常见误区是「用户输入要防注入,RAG 检索的文档来自自家库,可信」。错。库里的文档会被投毒、会本就来自用户上传、会从公网爬取。任何进入 prompt 的非系统内容都按不可信处理,包括检索结果、工具返回、网页正文。间接注入正是钻「以为数据可信」的空子。

数据合规(国内尤需注意)

除注入防御外,生产还要处理合规——PII 脱敏与最小化收集、数据跨境 / 出境限制、训练与检索语料的授权,以及国内的等保备案要求。面试官(尤其甲方 / 金融 / 政企方向)常问「用户数据怎么保证不外泄、不违规」。

4.6可靠性:重试风暴、熔断、结构化校验

生产里最常见的事故不是答错,而是重试风暴——失败的步骤被重试,重试又把上下文二次撑大,成本和延迟一起爆炸。

为什么需要它

Agent 的自主循环天生脆弱:一次工具失败触发重试,重试时把失败的上下文也带进去,下一轮 prompt 更长、更贵、更容易再失败——形成正反馈。这是一笔成本 / 延迟的账,不是质量的账,但它是真实线上事故的头号来源。可靠性工程就是给这个循环装上刹车。

三件套:超时、有选择的重试、按模式熔断。① 给每次 LLM / 工具调用设 30–60s 超时,避免一个挂起的请求拖死整条链。② 重试要有选择:只对 429(限流)和 5xx(服务端错误)重试,4xx(参数错、内容违规)重试只是白烧钱;且必须指数退避 + 抖动(jitter),否则所有客户端同时重试会把上游打垮(惊群)。③ 熔断按「模式」触发,而非只看频率——检测「同样的调用在循环」「成本速率突增」这类模式,比单纯「错误率超阈值」更能抓住重试风暴和死循环。

工具 / LLM 调用失败 timeout / 5xx 无脑重试 带着失败上下文重发 上下文二次膨胀 prompt 越来越长 成本↑ 延迟↑ 更易失败 正反馈 失败即重试 带入下一轮 prompt 变长 → 更贵更慢 再次失败 熔断在此切断
图 4.5重试风暴是个正反馈环:失败→重试→上下文膨胀→更贵更慢更易失败→再重试。注意:这是成本/延迟 bug,不是质量 bug;熔断要按「成本速率突增 / 同样调用循环」这类模式切断(图中黑线),只看错误率频率会抓不住。

结构化输出:先用原生保证,再用校验兜底。要让 LLM 稳定吐出能被程序消费的 JSON,首选 §1.5 的原生 strict schema / 约束解码(token 级构造性保证 schema,根本不给"吐出非法结构"留机会);只有当接口不支持原生约束、或要校验 schema 之外的业务语义(取值范围、跨字段一致性)时,才退到下面这条更弱的"事后校验 + 重试"。三层校验:schema 层(字段、类型对不对)→ 结构层(嵌套、必填齐不齐)→ 内容层(取值在合法范围内)。用 Pydantic 解析 + 带错误反馈的重试(上限 3 次)——把校验失败的具体报错回灌给模型让它自我修正——这是工程经验值,例如能把「无法解析」的比例压到千分位级别(具体随模型与 schema 复杂度而变)。对有副作用的工具调用还要加幂等键,保证重试不会重复扣款 / 重复下单。

validate_retry.py(演示用,未本机执行) Python
# Pydantic 校验 + 带错误反馈的重试 —— 演示用,未本机执行
from pydantic import BaseModel, ValidationError, field_validator

class Order(BaseModel):
    sku: str
    qty: int
    @field_validator("qty")
    @classmethod
    def positive(cls, v):
        if v <= 0:                      # 内容层校验:取值必须合法
            raise ValueError("qty 必须为正")
        return v

def extract_order(llm, prompt: str, max_retry: int = 3) -> Order:
    err = ""
    for attempt in range(max_retry):
        raw = llm.complete(prompt + err)        # 把上一次的报错回灌给模型
        try:
            return Order.model_validate_json(raw)   # schema+结构+内容 三层一次过
        except ValidationError as e:
            # 关键:不是简单重试,而是带着具体错误让模型自我修正
            err = f"\n\n上次输出不合法:{e}。请仅返回符合 schema 的 JSON。"
    raise RuntimeError("超过重试上限仍无法解析")   # 兜底,不让坏数据流入下游
err 回灌把校验报错拼回 prompt,是「带反馈重试」和「无脑重试」的本质区别——后者只会重复同样的错。 max_retry=3重试有上限,超限即兜底失败,避免变成重试风暴。
陷阱 · 重试不加抖动 = 自制 DDoS

上游一抖动返回一批 429,如果所有客户端都按「固定 1s 后重试」,它们会在同一时刻再次集体打过去(惊群效应),把刚要恢复的上游再次打挂,循环往复。重试必须叠加随机抖动——退避时间 = 基础退避 × 2^n + random(0, jitter),把重试时刻打散开。只重试 429/5xx、限次数、加抖动,三条缺一不可。

4.7框架选型:LangChain / LangGraph / AutoGen / CrewAI

框架选型的主轴是「你需要多强的控制力」:快速原型用 LangChain,要细粒度控制和持久化就升到 LangGraph,多 Agent 协作看 AutoGen / CrewAI。

为什么需要它

选错框架的代价是中途重写。LangChain 的链式抽象让 demo 飞快,但当业务需要循环、条件分支、断点续跑、人工介入时,它的 AgentExecutor 偏黑盒、循环控制弱,会逼你绕一堆 hack。面试官问选型,考的是你能不能说清「从 Chain 升到 Graph 是被什么需求逼的」——这正是 demo 到生产的分水岭。

四个框架的定位。LangChain:链式编排,快速原型首选,生态最大;短板是 AgentExecutor 黑盒、缺细粒度循环 / 状态控制。LangGraph:把流程建模成图(节点 + 边 + 状态),支持 checkpoint 持久化、人工节点(human-in-the-loop)、多 Agent、时间旅行 / 断点调试——生产级控制力。AutoGen(微软):会话式 GroupChat,多个 Agent 像开会一样轮流发言协作。CrewAI:角色化 crew,给每个 Agent 派角色(研究员 / 写手 / 审核)分工协作。

表 4.6 · 框架选型(按控制力需求升级)
框架适合什么代价 / 短板
LangChain快速原型、线性链、生态丰富AgentExecutor 偏黑盒、循环 / 状态控制弱,复杂流程要 hack
LangGraph生产级单 Agent / 多 Agent:图状细粒度控制、checkpoint 持久化、人工节点、时间旅行调试学习曲线陡、要自己设计状态图,原型阶段偏重
AutoGen会话式多 Agent(GroupChat 协作)会话编排难精确控制、成本随对话轮次涨
CrewAI角色化多 Agent 分工协作,上手快抽象偏高层,细粒度控制和可观测性弱
洞察 · 从 Chain 升到 Graph 的触发点

需要这些能力时,就该从 LangChain 升到 LangGraph:① 流程有循环和条件分支(ReAct 式「想-做-看」要反复跑);② 需要断点续跑(长任务挂了能从 checkpoint 恢复,而不是从头来);③ 需要人工介入(敏感步骤暂停等人确认);④ 需要可调试(时间旅行回放每一步状态)。这些恰好都是生产要求而原型不要求的——所以「升级」本质是「从 demo 转生产」。

§面试题检索

先合上教程,把答案在心里过一遍或写下来,再点开对照。直接展开等于把这一章当再读一遍。标「高频」的是社招必问。

  1. [高频][系统设计] 怎么设计一个大模型应用的后端架构?
    参考答案

    七层:流式层(SSE/WS,一开始就设计进去)+ 多级缓存(精确 / 语义 / 前缀)+ 多模型路由降级(简单任务小模型、按置信升级)+ 异步编排(队列 / 批处理收割非实时流量)+ 可观测(Langfuse 记 trace/span:prompt/token/成本/延迟)+ token 配额(按用户 / 租户限速防失控)+ 安全(输入输出护栏、最小权限工具)。若用自托管模型,还要有一层推理服务(vLLM 等,靠 continuous batching + PagedAttention 提吞吐)。

    追问「哪层先做」:先做可观测(没数据无法优化)和成本路由(收益最大)。追问「流式为什么一开始就设计进去」:它是贯穿浏览器→CDN→反代→应用的端到端管道,后期补要改全链路配置,且多步 Agent 不流式体感就是卡死。

  2. [高频] 流式输出怎么实现?SSE vs WebSocket 怎么选?
    参考答案

    生成慢(数秒)就必须流式。SSE:纯 HTTP 单向、实现简单、代理友好(记得关 Nginx proxy_buffering),默认选它。WebSocket:双向,需要生成途中中断 / 审批 / 转向时才用。HTTP 200 发出后再出的错改不了状态码,要走流内自定义错误事件让前端解析。

    追问:①统计 token——流式下 usage 不在响应头,要边推边累加或读结束帧;②中途报错——流内 error 事件;③停止生成——SSE 下另开取消请求,WebSocket 下直接发中断消息。

  3. [高频] token 成本太高怎么优化?
    参考答案

    按优先级:模型路由(简单任务小模型,据厂商数据可省下可观成本,实际看任务分布与阈值)→ 上下文压缩 → prompt 前缀缓存(读取约 0.1× = 9 折,缓存静态前缀)→ 语义缓存(命中 25–35%)→ 减少工具调用 / 轮次 → 批处理(Batch 5 折,可叠加缓存到约 75%)。

    追问「不降质前提下怎么省」:路由 + 前缀缓存 + 批处理这三样几乎不动质量;语义缓存会答错要放最后配阈值。追问「成本看板怎么搭」:在每个生成型 span 上记模型 + token + 成本,按模型 / 用户 / 租户聚合出燃烧率。

  4. [高频] 自托管推理服务怎么提升吞吐?vLLM 的 PagedAttention 和 continuous batching 解决什么问题?
    参考答案

    大模型推理的瓶颈是显存(KV cache,见 01 章 §1.1)和 GPU 利用率。PagedAttention 借鉴操作系统虚拟内存分页来管理 KV cache,按需分配、减少碎片,让更多请求共享显存;continuous batching(连续批处理)在每个解码步动态把新到的请求拼进批次,不必等整批跑完,显著提升吞吐。

    追问:vLLM / SGLang / TensorRT-LLM 怎么选?为什么长上下文请求会拉低整体吞吐?

  5. [高频] 怎么做可观测性?
    参考答案

    三层:链路追踪(Langfuse / LangSmith 记每个 span 的 prompt / 输出 / 模型 / token / 延迟 / 成本,span = 一个工作单元)+ 指标(P99 延迟、错误率、token 燃烧率、缓存命中率)+ 审计(谁何时问了什么、触发哪些工具)。

    追问「效果衰减怎么持续监控」:采样线上输出喂 online 评估,盯质量趋势线。追问「怎么定位哪一跳变慢变贵」:看 trace 调用树,每个 span 的延迟和成本一目了然,最慢 / 最贵那一跳即瓶颈。

  6. [高频] 怎么评估一个 Agent?为什么比评 LLM 难?
    参考答案

    难在:多步、路径不唯一、中间步骤难判对错。所以除最终正确性还要看过程指标(步数、工具调用准确率、成本)。方法:LLM-as-judge + benchmark,配 offline golden set 和 online 灰度。除自建 golden set + RAGAS 外,还有公开 benchmark 做横向能力评估——SWE-bench(代码任务)、GAIA(通用 agent)、τ-bench(工具调用 / 对话)、AgentBench、MMLU(知识)。

    追问「怎么搭离线 eval 防 prompt 改动回归」:固定 golden dataset 进 CI/CD 当 gate,改动合并前跑分,掉分就拦。追问「LLM-as-judge 有哪些偏见」:位置偏见(60–75%)、啰嗦偏好、自我偏好;缓解 = 交换 A/B 顺序跑两遍取一致 + 人工标注校准。

  7. [高频] 什么是 Prompt 注入?怎么防御?
    参考答案

    用户 / 外部内容篡改系统指令,根因是指令与数据共用一条通道。直接注入(「忽略之前指令」)vs 间接注入(藏在 RAG 文档 / 网页里)。OWASP LLM01 头号风险。防御(纵深):指令与数据分隔、最小权限工具、沙箱、输出审核、HITL、不信任检索内容。致命三件套 = 不可信输入 + 私有数据 + 外泄通道,凑齐才致命。

    追问「间接注入怎么防」:把检索结果 / 工具返回 / 网页正文一律当不可信数据;砍掉致命三件套的一条边(如对能读私有数据的 Agent 关出网通道);拦截 markdown 外链图片这类走私式外泄。

  8. [高频] LangChain vs LangGraph vs AutoGen vs CrewAI 怎么选?
    参考答案

    LangChain:链式快速原型(短板:缺循环控制、AgentExecutor 黑盒)。LangGraph:图状细粒度控制 + checkpoint 持久化 + 人工节点 + 多 Agent + 时间旅行调试。AutoGen:会话式 GroupChat 多 Agent 编排。CrewAI:角色化 crew 协作。

    追问「为什么从 Chain 升到 Graph」:当需要循环 / 条件分支、断点续跑、人工介入、可调试——这些都是生产要求而原型不要求的,所以「升级」本质就是「转生产」。

  9. 生产环境的 Agent / LLM 应用踩过哪些失败模式?
    参考答案

    死循环 / 失控成本、工具选错、上下文爆炸、幻觉调用不存在的工具、错误累积。最常见的生产事故其实是重试风暴导致上下文二次膨胀——是成本 / 延迟 bug,不是质量 bug。

    对策:熔断按模式触发(循环 / 成本速率突增)而非只看频率、限制步数、上下文压缩、结构化校验、带抖动的指数退避且只重试 429/5xx。追问「具体某个失败怎么定位解决」:从可观测数据出发——看 trace 找出反复出现的 span,对比成本曲线和上下文长度曲线,锁定是重试 / 缓存未命中 / 上下文膨胀中的哪一个。

进阶挑战 · 刚好够不着

账单涨了 3 倍,质量没变,怎么排查到根因?

你的 Agent 月账单突然涨了 3 倍,但抽样看输出质量没有明显变化。给出一个从可观测数据出发、定位到根因的排查顺序——不要凭感觉猜,要说清每一步看哪个指标、能排除什么。

提示(卡住再展开)

质量没变但成本暴涨,说明是「同样的事花了更多 token」——大概率不是质量 bug 而是结构 bug。沿着这条线索想三个嫌疑:① 重试风暴(看错误率 + 单 turn 的 span 数是否变多);② 上下文膨胀(看平均 input token 是否上涨);③ 缓存失效(看缓存命中率是否掉了,可能是前缀里混进了每次都变的内容)。排查顺序:先看 token 燃烧率按「输入 / 输出」拆分 → 看单次 turn 的平均 span 数和重试次数 → 看缓存命中率趋势 → 下钻到具体 trace 看是哪类请求贡献了增量。