Skip to content

AI 应用系统设计

到这里,你不再只是在"调一个模型接口",而是在设计一个由前端、后端、模型、检索、工具、安全和评测共同组成的系统。这一章是整个主线的收口。

前面十一章每一章讲了一个维度:对话、工具、RAG、Agent、幻觉、安全、评测、多模态。这一章把它们放回一个真实系统里,说清楚它们各自在系统里的位置,以及为什么缺一不可。

本章目标

  • 能画出一个最小 AI 应用的模块边界
  • 明白为什么 Prompt、RAG、工具、安全、日志和评测都应该被当作一等模块
  • 能列出上线前最基础的工程检查项
  • 理解 AI 系统为什么比普通 CRUD 系统更需要失败路径设计

AI 应用为什么不能只当成"普通接口接入"

普通 CRUD 系统主要面对确定性规则:同样的输入,给出同样的输出,出错了能捕获异常。

AI 系统会额外面对:

  • 输出不稳定:同样的问题,不同时间回答可能不同
  • 幻觉:模型可能生成看起来合理但不真实的内容
  • 上下文管理:对话轮次越多,上下文越复杂,截断和压缩是工程问题
  • 工具调用风险:模型判断失误可能触发错误动作
  • 检索质量波动:RAG 的召回质量不稳定,直接影响回答
  • 成本和时延不稳定:不同任务消耗的 token 数量差距很大

所以 AI 系统里,模型只是核心能力之一,不是全部。其余的工程模块同样重要。

再往下想一层:普通系统主要在管理确定逻辑,AI 系统管理的是不确定性

你不再只是关心"接口能不能返回结果",而是要同时关心:

  • 这个结果有没有依据
  • 这个依据是不是足够
  • 模型有没有越界发挥
  • 这一次输出变好还是变差了
  • 当前成本和延迟是不是还可接受

所以 AI 应用设计的难点,不是单纯接上模型,而是把这种不确定性收编进工程结构里。

一个最小 AI 应用通常包含哪些模块

前端交互层

负责:

  • 输入问题
  • 展示流式输出
  • 展示引用(资料来自哪里)
  • 展示工具调用过程(用户看到 Agent 在做什么)
  • 给高风险动作做确认("确认要发送这封邮件吗?")

前端不只是"展示回答",还承担可信交互设计——让用户知道答案从哪来、系统在做什么、哪里不确定。

后端编排层

这是 AI 应用的中枢,负责把用户请求变成一串系统动作:

  1. 接收用户问题
  2. 判断是否需要检索
  3. 如果需要,召回资料
  4. 判断是否需要工具
  5. 组织上下文
  6. 调用模型
  7. 解析输出
  8. 记录日志并返回前端

如果是 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 应用更实用的画法,是围绕四个核心对象来看:

  1. 上下文:这一轮模型到底看到了什么
  2. 状态:系统当前做到哪一步、已经发生了什么
  3. 边界:哪些能力允许模型建议,哪些能力必须由系统把关
  4. 证据:最终输出依据什么而来,能不能被追溯

你会发现,前面那些章节其实都在服务这四个对象:

  • Prompt 和记忆影响上下文
  • Agent 和压缩影响状态
  • 安全和权限定义边界
  • RAG、引用、评测在强化证据

这比只记模块名更有用,因为它更接近真实设计时的思考顺序。

整本书的脉络

到这里,主线章节全部读完。回头看,这十二章讲的是一个完整的工程认知链:

  1. 对话基础(LLM 基础概念、Prompt 工程):模型是什么,怎么控制输出
  2. 结构化能力(Structured Output、Tool Calling):怎么拿到可编程的结构化结果,怎么调工具
  3. 知识接入(Embedding 与向量检索、RAG 原理):怎么让模型"会查资料",RAG 完整链路
  4. 多步执行(Agent 基础原理):从单步到多步,Agent 的能力和风险
  5. 系统风险(AI 幻觉、Prompt Injection 与 AI 安全):幻觉和注入攻击,为什么必须主动防
  6. 质量保障(AI 应用评测):评测、回归、测试套件
  7. 扩展能力(多模态基础):多模态,什么时候引入
  8. 系统收口(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 Promptconstants/prompts.ts 运行时组装 + Feature Flag
Tool Calling → 40+ 工具系统tools/ 目录,每个工具有独立权限和安全逻辑
Agent → Agent LoopQueryEngine.ts 核心循环 + coordinator/ 多 Agent 协调
AI 安全 → 三层安全闸门BashTool/ 的 AST 分析 + AI 分类 + 沙箱判断
AI 评测 → 工具调用轨迹services/toolUseSummary/ 记录每次工具调用的过程

AI 应用不只是调 API,它是前端、编排、模型、数据、日志、安全六层共同支撑的工程系统,每一层从第一天起就该是独立模块。

做过之后才会有的体感

这一章不只是收尾总结,它更像是一张检查清单。

AI 系统设计的核心不是把模型接进来,而是把不确定性组织起来。模型不是总控——编排层和安全层才决定系统真正会怎么行动。

日志、评测、回滚看起来是辅助模块,但它们提供的是系统的自我修正能力。一旦缺了它们,问题会悄悄积累,直到不可收拾。AI 应用最怕的就是这种静默失败,所以可观测性在这里比普通系统更关键。

最后一个检验标准:你画架构图的时候,能不能标出上下文、状态、边界和证据这四个对象。如果标不出来,系统设计大概率还没想清楚。

自查

读完这章,试着回答:

  1. 为什么 AI 应用不是只加一个模型接口?
  2. 编排层在系统里扮演什么角色?
  3. 为什么前端在 AI 产品里不只是展示层?
  4. 为什么日志、评测和安全都要被当成一等模块?
  5. 为什么 AI 系统必须先设计失败路径?
  6. 向一个刚入门的同学解释"安全层为什么不是事后补的",你会用 Claude Code 的哪个模块举例?

补充自测

  1. 如果你发现 AI 客服系统最近回答质量下降,但没有任何报错,你会按什么顺序排查?

  2. 你要给一个 RAG 问答系统加上可观测性,让运维能第一时间发现问题,你会在系统里加哪些检测点?

学完主线后的下一步

回到 AI 应用开发路线 复盘主线,然后开始做三个项目页里的练手项目,把执行状态更新到 学习路线图进度总览

第三阶段的综合练习是 Research Agent——把 Agent、安全、评测这几章的能力真正落地。

面向开发者的 AI 实战路线——Vibe Coding 与 AI 应用开发