Appearance
AI 应用系统设计
到这里,你不再只是在"调一个模型接口",而是在设计一个由前端、后端、模型、检索、工具、安全和评测共同组成的系统。这一章是整个主线的收口。
前面十一章每一章讲了一个维度:对话、工具、RAG、Agent、幻觉、安全、评测、多模态。这一章把它们放回一个真实系统里,说清楚它们各自在系统里的位置,以及为什么缺一不可。
本章目标
- 能画出一个最小 AI 应用的模块边界
- 明白为什么 Prompt、RAG、工具、安全、日志和评测都应该被当作一等模块
- 能列出上线前最基础的工程检查项
- 理解 AI 系统为什么比普通 CRUD 系统更需要失败路径设计
AI 应用为什么不能只当成"普通接口接入"
普通 CRUD 系统主要面对确定性规则:同样的输入,给出同样的输出,出错了能捕获异常。
AI 系统会额外面对:
- 输出不稳定:同样的问题,不同时间回答可能不同
- 幻觉:模型可能生成看起来合理但不真实的内容
- 上下文管理:对话轮次越多,上下文越复杂,截断和压缩是工程问题
- 工具调用风险:模型判断失误可能触发错误动作
- 检索质量波动:RAG 的召回质量不稳定,直接影响回答
- 成本和时延不稳定:不同任务消耗的 token 数量差距很大
所以 AI 系统里,模型只是核心能力之一,不是全部。其余的工程模块同样重要。
再往下想一层:普通系统主要在管理确定逻辑,AI 系统管理的是不确定性。
你不再只是关心"接口能不能返回结果",而是要同时关心:
- 这个结果有没有依据
- 这个依据是不是足够
- 模型有没有越界发挥
- 这一次输出变好还是变差了
- 当前成本和延迟是不是还可接受
所以 AI 应用设计的难点,不是单纯接上模型,而是把这种不确定性收编进工程结构里。
一个最小 AI 应用通常包含哪些模块
前端交互层
负责:
- 输入问题
- 展示流式输出
- 展示引用(资料来自哪里)
- 展示工具调用过程(用户看到 Agent 在做什么)
- 给高风险动作做确认("确认要发送这封邮件吗?")
前端不只是"展示回答",还承担可信交互设计——让用户知道答案从哪来、系统在做什么、哪里不确定。
后端编排层
这是 AI 应用的中枢,负责把用户请求变成一串系统动作:
- 接收用户问题
- 判断是否需要检索
- 如果需要,召回资料
- 判断是否需要工具
- 组织上下文
- 调用模型
- 解析输出
- 记录日志并返回前端
如果是 Agent 系统,编排层还要管理状态、步数、终止条件和安全确认。
这里要特别强调一下:编排层不是"顺手写几个 if else 把接口串起来",它本质上是 AI 系统的控制中枢。
下面这段骨架代码不是生产代码,而是帮你看清楚"编排层到底在管什么"——每个 if 分支都是一个真实工程决策点:
python
async def orchestrate(user_input: str, session: Session) -> Response:
# 从会话历史提取上下文(可能已经过压缩)
history = session.get_compressed_history()
# 判断是否需要检索:这里可以是规则,也可以是轻量分类模型
context_chunks = []
if needs_retrieval(user_input):
raw_chunks = await vector_store.search(user_input, top_k=5)
context_chunks = filter_low_relevance(raw_chunks)
# 如果什么都没检索到,后续应该拒答而不是让模型自由发挥
if not context_chunks:
return fallback_response("未找到相关资料,无法回答")
# 组装 messages:顺序影响模型注意力
messages = build_messages(
system_prompt=get_current_prompt_version(), # Prompt 版本受配置管理
history=history,
context=context_chunks,
user_input=user_input,
)
# Agent 循环:工具调用可能触发多轮
step = 0
while step < MAX_STEPS:
response = await model.call(messages, tools=available_tools)
if response.finish_reason == "tool_call":
tool_result = await execute_tool(response.tool_call)
if tool_result.error:
# 工具调用失败不能静默忽略,要决定是重试、降级还是终止
log_tool_failure(tool_result)
return fallback_response("工具执行失败")
messages.append(tool_result_as_message(tool_result))
step += 1
continue
# 非工具调用,退出循环
break
else:
# 超出最大步数,强制终止
return fallback_response("超出最大执行步数")
# 解析输出:期望结构化格式时要验证,失败要重试或降级
parsed = parse_output(response.content)
if parsed is None:
retry_response = await model.call(messages + [format_error_hint()])
parsed = parse_output(retry_response.content)
if parsed is None:
return fallback_response("输出格式异常")
# 日志要在返回之前记,记失败了不影响主流程但要告警
await log_request(
session_id=session.id,
input=user_input,
output=parsed,
prompt_version=get_current_prompt_version_id(),
tokens=response.usage,
latency_ms=response.latency_ms,
context_sources=[c.source for c in context_chunks],
)
return Response(content=parsed, sources=context_chunks)每一个 if 背后都有真实问题:检索判断决定了回答有没有依据,工具循环要有步数上限否则可能失控,输出解析失败要处理否则直接崩到前端,日志要记才能在问题出现后有据可查。编排层的价值就在这里:它把所有这些决策显式化,而不是让它们散落在各处。
前端负责交互,模型负责生成,但"什么时候检索、什么时候调工具、什么时候拒答、什么时候要求确认、什么时候降级"这些真正影响系统行为的决策,通常都落在编排层。
模型层
负责自然语言理解、生成、结构化输出和工具决策。
需要注意的是:模型能做什么,和系统允许它做什么,是两件不同的事。系统设计的职责就是划清这条边界。
这也是很多团队最容易犯错的地方。模型能力一强,就容易下意识把它当成系统中心,仿佛别的层都只是为它服务。其实在成熟系统里,模型更像高能力但高不确定性的推理引擎,而不是总控。
数据与知识层
包括:
- 业务数据库
- 向量库(RAG 所需)
- 文件存储(用户上传文档)
- 文档索引
日志与评测层
记录:
- 输入输出
- Prompt 内容
- 检索命中情况
- 工具调用轨迹
- token 用量和耗时
- 测试结果
日志不只是调试用的,还是发现问题、做评测、回滚决策的基础。
对 AI 系统来说,日志的意义甚至比普通系统更大。因为很多失败不是报异常,而是"看起来能工作,但悄悄变差了"。没有轨迹和评测,你往往根本不知道是 Prompt 退化了、检索坏了、模型换版本了,还是工具链哪一步开始漂移。
安全与权限层
负责:
- 权限校验
- 高风险操作确认
- Prompt Injection 防护
- 输出过滤
- 审计日志
安全层应该在模型之外独立实现,而不是写在 Prompt 里打补丁。
一次用户请求,系统内部是怎么流转的
把六层模块逐一介绍之后,还有一件事值得单独讲:它们在运行时是什么顺序被调用的,中间有哪些真实的判断分支。
请求从用户输入开始。前端把用户的文字(或者语音、图片,如果是多模态应用)打包成一条结构化请求,带上会话 ID 和历史轮次标识,发给后端。这里的"前端"不单是 Web 页面,CLI 工具、移动端、企业系统里嵌入的对话框都是这一层。
请求到达编排层之后,第一件事不是立刻调模型,而是做路由判断:这个查询需不需要检索外部知识? 有些应用会做一个简单的分类器(关键词匹配或用轻量模型做意图识别),有些直接用规则:只要查询和产品文档相关就先过一遍 RAG,否则直接带历史上下文提问。如果跳过这一步,对于依赖私域知识的场景,模型会在没有依据的情况下开始即兴发挥,而你不会从输出里看出任何异常。
如果需要检索,编排层把查询发给向量库,拿回若干段落,再做一轮过滤——去掉相关度太低的、去掉明显重复的,把剩余内容按来源整理好,准备拼进上下文。检索这步出问题(没拿到关键段落、或者返回了过时内容),模型完全可能给出看起来流畅但依据错误的回答,而且整个链路没有任何报错。
接下来判断是否需要工具。如果是简单问答,上下文拼好就可以调模型了。如果是 Agent 场景或涉及实时数据,编排层要决定要不要在第一次调模型时就把可用工具列表传进去,让模型自己决策是否调用。工具调用本质上是模型返回的一段结构化指令,告诉编排层"我需要执行某个工具,参数是 X",编排层收到后真正去执行,再把执行结果塞回上下文,然后进入下一轮模型调用。
这就是 Agent 循环的核心:编排层 → 模型 → 工具调用 → 执行结果返回编排层 → 再次调模型。这个循环可能只跑一轮,也可能跑十几轮。编排层必须设定步数上限和终止条件,否则一个判断失误就能让循环跑起来停不下来。
上下文组装是编排层最精细的部分。一般顺序是:系统提示词(System Prompt,定义模型角色和行为规则)→ 历史对话(经过压缩处理,长对话会有摘要替代原始轮次)→ 检索到的资料(带来源标记)→ 工具执行结果(如果有)→ 当前用户输入。这个顺序影响模型的关注优先级,乱排会导致模型对某些信息视而不见。
上下文拼好后调用模型。这里要处理两种返回模式:流式(边生成边传回)和非流式(生成完整再传)。流式是对话产品的主流,因为它让用户感觉响应更快,但流式场景下解析结构化输出(比如 JSON)会更麻烦——你必须等整个输出结束才能解析,或者实时做部分解析。
模型返回后,编排层解析输出。如果期望是纯文本,只需判断是否有违规内容。如果期望是结构化格式(JSON、表格),需要做格式校验;格式出错时要决定是直接重试还是降级到纯文本返回。这里的重试逻辑很多团队没有写,出了问题直接崩给前端一个 500,用户看到的是一个神秘报错。
输出通过校验后,记录日志——输入、最终上下文、模型输出、工具调用路径、token 消耗、耗时、本轮使用的 Prompt 版本号。日志记完,结果连同引用来源一起返回前端。
前端拿到结果后,不只是把文字渲染出来。如果带了引用资料,前端需要展示来源和可跳转的段落;如果是 Agent 模式,前端要展示工具调用过程,让用户看到系统"在做什么"而不是一个黑盒等待。这些不是锦上添花,是用户能否信任系统的关键。
整个流程走完,大多数情况下不会超过 3 秒。但在这 3 秒里,你的系统已经走过了至少八到十个真实决策点,每一个都可能成为系统行为失控的位置。
模块分层的真正意义:把不确定性关进笼子
为什么要这么多层?不是因为 AI 应用天生更复杂,而是因为不同类型的不确定性,需要交给不同模块处理。
- 模型层 处理语言与推理的不确定性
- 检索层 处理外部知识覆盖范围的不确定性
- 编排层 处理流程选择和失败分支的不确定性
- 安全层 处理不可信输入和危险动作的不确定性
- 日志评测层 处理"系统是不是正在变坏"这件事的不确定性
一旦你不分层,所有问题最后都会混在"模型好像不太行"这一句里,既没法定位,也没法治理。
为什么这些模块都是一等公民
常见的做法是:先把 AI 接口跑通,然后"后面再补安全""后面再加评测""后面再做日志"。这不是好主意,原因很直接:
- 没有日志,出了问题无法定位
- 没有评测,改了 Prompt 不知道是变好还是变差
- 没有安全,上线后随时可能被注入攻击
- 没有失败路径,第一次模型输出异常就会崩
第一版就应该有这些模块的骨架,而不是等"功能完善"再补。
很多 AI 项目之所以后期很难救,不是因为模型不够强,而是第一版只做了"理想路径",没给系统留出观察、限制、回退和审计的位置。等问题一多,再回头补这些层,代价会非常高。
为什么 Prompt 应该被当成配置管理
Prompt 决定系统行为,应该像代码或配置一样被:
- 版本化(知道当前用的是哪个版本)
- 评测(知道这个版本的效果如何)
- 回滚(效果变差时能回到上一个版本)
- 集中管理(不要散落在多个文件里随手拼接)
把 Prompt 当成随意字符串来管理,是大量 AI 应用出问题的根源之一。
实际上,Prompt 在 AI 系统里的地位已经很接近业务规则和策略配置。它直接影响:
- 回答风格
- 拒答阈值
- 工具选择倾向
- 引用和结构化输出方式
- 安全边界的语言表达
既然它会影响系统行为,就不能再把它当成一段随手拼接的文案。
AI 系统更需要失败兜底
AI 系统的失败通常不会报程序错误,而是"悄悄失败":
- 模型输出了格式错误的 JSON,下游解析崩了
- 工具调用超时,Agent 一直等
- 检索不到内容,模型开始编答案
- 高风险动作在没有确认的情况下被执行了
你必须提前设计每条失败路径:
- 模型输出格式错了 → 重试或降级
- 工具超时 → 超时兜底,记录日志
- 检索不到内容 → 拒答并提示
- 资料不足 → 返回"不确定"而不是继续发挥
- 高风险动作 → 中断并等待用户确认
失败路径设计往往比理想路径更重要。真实用户会以你意想不到的方式使用系统。
为什么 AI 系统最怕“悄悄失败”
普通系统很多错误会直接抛出来,比如 500、超时、空指针。AI 系统更烦的地方在于,它很擅长把失败伪装成正常输出。
比如:
- 检索没拿到关键资料,但模型仍然给了一个顺滑回答
- 工具调用超时后,模型把旧结果继续当成当前结果解释
- Prompt 改动后,结构没坏,但答案开始慢慢漂
这种失败不会吵闹,却最伤系统。因为它让团队在一段时间里误以为一切正常。
所以 AI 系统设计里,一个核心目标不是只让成功路径跑通,而是让失败能被看见、被截断、被回滚。
发现悄悄失败的排查路径
发现系统质量下降,但没有任何报错,应该怎么排查?这不是玄学,可以按层逐步缩小范围。
第一步:定位失败类型。 先看问题是什么形状——是输出格式异常(JSON 解析失败、字段缺失),还是内容质量下降(答案看似合理但错了),还是性能劣化(响应时间明显变慢)。三种类型对应不同方向,混在一起排查效率很低。
第二步:查检索命中情况。 如果是内容质量问题,先从日志里翻这几个查询的检索结果——召回了哪些段落,相关度分数是多少,有没有目标段落根本没被召回。大量悄悄失败都源于检索层:文档更新了但向量库没有重建,或者 embedding 模型换版本后相似度分布变了。把问题查询的检索结果和几个月前同类查询对比,往往能直接看出来。
第三步:对比 Prompt 版本。 看 Prompt 变更记录,确认最近有没有改动。一个"微小的措辞调整"有时候会在特定场景下产生显著的行为变化,而且通常很隐蔽。如果有评测数据集,对有问题的查询用旧版 Prompt 重跑一遍,看结果是否恢复正常,就能快速定位是不是 Prompt 引起的。
第四步:隔离模型版本变化。 很多云端 AI 服务供应商会静默升级基础模型(比如从 gpt-4o 的某个内部版本升到另一个),不一定会主动通知。检查日志里记录的模型版本字段,对比问题出现前后是否一致。如果没有记录这个字段,这就是你现在应该加进去的。
第五步:检查工具链静默错误。 如果问题是某类需要工具调用的查询在出问题,检查工具执行日志——有没有超时记录、有没有返回空结果后被静默忽略、有没有某个外部 API 开始偶发 503。工具链的静默错误经常被遗漏,因为编排层容错了,但工具结果已经是错的,模型就在错误信息上继续推理。
排完这五步,大多数悄悄失败都能找到根因。如果仍然排不出来,下一步是把有问题的请求完整复现(原始输入 + 当时的上下文快照 + 工具调用记录),在本地逐步调试。
上线前最基础的检查项
- 密钥和环境变量隔离(不在代码里硬编码 API Key)
- 限流和错误提示(避免成本爆炸,告知用户失败原因)
- 日志和审计(至少记录输入输出和工具调用)
- 关键功能回归测试(跑一遍评测数据集)
- 高风险工具确认(不可逆动作前必须暂停)
- Prompt / RAG / Tool 配置可回滚(能快速恢复到上一个稳定版本)
- 上下文截断边界测试(发长文本、粘贴大段代码时,系统如何截断,截断后有没有告知用户)
- 向量库健康检查(embedding 数据是否最新,有没有过期文档没有重建,索引更新是否有延迟监控)
- 速率限制与超时配置(API 调用有没有频率上限,有没有配置请求超时,超时后是否有兜底逻辑)
- 模型版本锁定(有没有锁定到具体模型版本号,避免供应商静默升级带来行为变化)
- 多轮对话压缩测试(长对话场景下触发压缩时,压缩后的历史能不能正常支撑后续轮次的问答)
- Prompt 注入样本测试(上线前至少跑几个常见注入攻击样本,确认安全层真的在生效)
一个更实用的视角:系统到底在围绕什么旋转
如果你以后真的要画架构图,我建议不要只画"前端 -> 后端 -> 模型 -> 数据库"这种传统箭头图。AI 应用更实用的画法,是围绕四个核心对象来看:
- 上下文:这一轮模型到底看到了什么
- 状态:系统当前做到哪一步、已经发生了什么
- 边界:哪些能力允许模型建议,哪些能力必须由系统把关
- 证据:最终输出依据什么而来,能不能被追溯
你会发现,前面那些章节其实都在服务这四个对象:
- Prompt 和记忆影响上下文
- Agent 和压缩影响状态
- 安全和权限定义边界
- RAG、引用、评测在强化证据
这比只记模块名更有用,因为它更接近真实设计时的思考顺序。
整本书的脉络
到这里,主线章节全部读完。回头看,这十二章讲的是一个完整的工程认知链:
- 对话基础(LLM 基础概念、Prompt 工程):模型是什么,怎么控制输出
- 结构化能力(Structured Output、Tool Calling):怎么拿到可编程的结构化结果,怎么调工具
- 知识接入(Embedding 与向量检索、RAG 原理):怎么让模型"会查资料",RAG 完整链路
- 多步执行(Agent 基础原理):从单步到多步,Agent 的能力和风险
- 系统风险(AI 幻觉、Prompt Injection 与 AI 安全):幻觉和注入攻击,为什么必须主动防
- 质量保障(AI 应用评测):评测、回归、测试套件
- 扩展能力(多模态基础):多模态,什么时候引入
- 系统收口(AI 应用系统设计):把所有模块放回完整系统
每一章都有它的位置,不是堆砌,而是你做真实 AI 产品必须依次面对的问题。
用一个真实产品串起六层模块
前面讲的六层模块,读起来像教科书分类——真实产品里是这样的吗?
Claude Code 是一个很好的对照物。它不是内部工具,npm 包已公开发布(@anthropic-ai/claude-code v2.1.88),源码结构可以还原研究。下面用它来对照一遍,你会发现六层模块在它身上一个不少,而且每一层都是独立的工程模块,而不是功能缝隙里临时插入的代码。
| 章节定义的模块 | Claude Code 的实际实现 |
|---|---|
| 前端交互层 | components/(80+ 个 React/Ink 终端 UI 组件):FileEditToolDiff.tsx 展示代码 diff、CoordinatorAgentStatus.tsx 展示多 Agent 协作状态、ContextVisualization.tsx 可视化上下文用量 |
| 后端编排层 | QueryEngine.ts(Agent Loop 核心)+ coordinator/(多 Agent 协调器):接收用户输入,决定调哪个工具,管理执行步数和终止条件 |
| 模型层 | services/api/ + query/:负责 API 调用与查询处理,包括 tokenEstimation.ts 估算成本 |
| 数据与知识层 | utils/claudemd.ts(四层优先级 CLAUDE.md 记忆)+ services/MagicDocs/(文档增强)+ services/SessionMemory/(会话记忆) |
| 日志与评测层 | services/analytics/(数据上报)+ services/toolUseSummary/(工具调用轨迹摘要) |
| 安全与权限层 | tools/BashTool/(bashPermissions.ts + bashSecurity.ts + bashClassifier.ts 三件套)+ 细粒度权限规则系统 |
两层值得单独说
安全层不是事后补的。 BashTool 是 Claude Code 里最复杂的单个工具之一,仅安全相关文件就有四个:bashPermissions.ts 对每条命令做 AST 语义分析,bashSecurity.ts 检测路径遍历和重定向滥用,bashClassifier.ts 用 AI 辅助分类每条命令是「允许 / 询问 / 拒绝」,shouldUseSandbox.ts 判断是否在沙箱中运行。这套机制从设计第一天就是独立模块,而不是"先让命令跑起来,再想怎么限制"。
数据与知识层也不只是"存储"。 CLAUDE.md 文件按四层优先级加载(企业级全局 → 个人记忆 → 项目规则 → 本地私有),services/compact/ 里的自动压缩服务会在上下文接近上限时触发,用 Claude 自身对历史对话做摘要压缩。这是数据层的实时治理,不是简单的向量库查询。
Prompt 是如何被当成配置管理的
src/constants/prompts.ts 运行时动态组装系统提示词——注入操作系统信息、当前 Git 状态、CLAUDE.md 记忆内容、已启用工具列表,甚至根据 Feature Flag(GrowthBook)动态开关功能段。这正是章节前面说的"Prompt 应该被版本化、评测、可回滚"——Claude Code 把这件事做到了代码级别,Prompt 不是一段写死的字符串,而是一个有结构的模板系统。
这个案例能帮你串起整本书的认知链
| 你在哪章学到的 | Claude Code 里的对应 |
|---|---|
| LLM 基础 → Token / Context 管理 | tokenEstimation.ts + services/compact/ 自动压缩 |
| Prompt 工程 → 动态 System Prompt | constants/prompts.ts 运行时组装 + Feature Flag |
| Tool Calling → 40+ 工具系统 | tools/ 目录,每个工具有独立权限和安全逻辑 |
| Agent → Agent Loop | QueryEngine.ts 核心循环 + coordinator/ 多 Agent 协调 |
| AI 安全 → 三层安全闸门 | BashTool/ 的 AST 分析 + AI 分类 + 沙箱判断 |
| AI 评测 → 工具调用轨迹 | services/toolUseSummary/ 记录每次工具调用的过程 |
AI 应用不只是调 API,它是前端、编排、模型、数据、日志、安全六层共同支撑的工程系统,每一层从第一天起就该是独立模块。
做过之后才会有的体感
这一章不只是收尾总结,它更像是一张检查清单。
AI 系统设计的核心不是把模型接进来,而是把不确定性组织起来。模型不是总控——编排层和安全层才决定系统真正会怎么行动。
日志、评测、回滚看起来是辅助模块,但它们提供的是系统的自我修正能力。一旦缺了它们,问题会悄悄积累,直到不可收拾。AI 应用最怕的就是这种静默失败,所以可观测性在这里比普通系统更关键。
最后一个检验标准:你画架构图的时候,能不能标出上下文、状态、边界和证据这四个对象。如果标不出来,系统设计大概率还没想清楚。
自查
读完这章,试着回答:
- 为什么 AI 应用不是只加一个模型接口?
- 编排层在系统里扮演什么角色?
- 为什么前端在 AI 产品里不只是展示层?
- 为什么日志、评测和安全都要被当成一等模块?
- 为什么 AI 系统必须先设计失败路径?
- 向一个刚入门的同学解释"安全层为什么不是事后补的",你会用 Claude Code 的哪个模块举例?
补充自测
如果你发现 AI 客服系统最近回答质量下降,但没有任何报错,你会按什么顺序排查?
你要给一个 RAG 问答系统加上可观测性,让运维能第一时间发现问题,你会在系统里加哪些检测点?
学完主线后的下一步
回到 AI 应用开发路线 复盘主线,然后开始做三个项目页里的练手项目,把执行状态更新到 学习路线图 与 进度总览。
第三阶段的综合练习是 Research Agent——把 Agent、安全、评测这几章的能力真正落地。