Skip to content

课 1 · 质量控制

本课目标

掌握大模型应用的常见质量问题和应对方案:输出校验、重试策略、Fallback 机制、Human-in-the-Loop。课后你会知道怎么让 AI 应用足够可靠。

如果你是第一次做上线版,这一课先抓住最低配必修项:输出校验、超时、重试、基础 fallback。Prompt 质量控制和 Human-in-the-Loop 是在此基础上的增强项,不要求首版一口气做满。

前面做的 Demo 都跑在本地,出错了大不了重来。上线的产品不行——用户不会给你第二次机会。

这一课的核心问题:模型输出不可控,怎么让应用足够稳定?

首版上线先保三件事就够了:别乱答、别挂死、别误执行高风险操作。 下面的内容也会按这个优先级来理解。

先看一张“最低配可靠性防线”图:

先抓最低配上线版

如果你现在只想把第一版安全上线,建议先完成这些:

  • 输出校验与结构化约束
  • 超时控制与重试策略
  • 基础 fallback

后面的 Prompt 质量控制和 Human-in-the-Loop 更像增强项:前者让回答更稳,后者让高风险动作更安全。

大模型应用的常见质量问题

问题表现频率
幻觉模型编造事实、提供错误信息
格式错乱JSON 结构不对、缺少必需字段
工具调用失败传错参数、调用不存在的工具
漏答 / 答非所问没回答核心问题,跑偏了
拒绝回答过度安全,该回答的也拒绝
延迟过高响应时间太长,用户等不了情况而异

和传统后端的区别:传统后端的 bug 是确定性的,修了就好。模型的问题是概率性的,不能"修",只能"防"。

输出校验与结构化约束

用 Zod Schema 约束输出

generateText + Output.object() + Zod Schema 是最有效的格式保障手段。模型输出不符合 Schema 时,SDK 会自动重试。

typescript
import { generateText, Output } from 'ai'
import { z } from 'zod'

const AnswerSchema = z.object({
  answer: z.string().min(10).describe('回答内容,至少10个字'),
  confidence: z.number().min(0).max(1).describe('置信度'),
  sources: z.array(z.string()).describe('引用来源'),
})

const { output } = await generateText({
  model,
  output: Output.object({ schema: AnswerSchema }),
  prompt: '什么是 RAG?',
})
// output 一定符合 AnswerSchema,否则会抛出错误

输出后再验证

有些逻辑校验 Schema 做不了,需要在拿到输出后二次检查。

typescript
function validateAnswer(answer: z.infer<typeof AnswerSchema>): {
  valid: boolean
  reason?: string
} {
  // 检查置信度过低
  if (answer.confidence < 0.3) {
    return { valid: false, reason: '置信度过低,可能是幻觉' }
  }
  // 检查来源为空
  if (answer.sources.length === 0) {
    return { valid: false, reason: '没有引用来源' }
  }
  return { valid: true }
}

const result = validateAnswer(output)
if (!result.valid) {
  // 降级处理:返回"我不确定"或重试
}

重试策略

模型调用会因为各种原因失败:网络超时、API 限流、输出格式错误。重试是最基本的容错手段。

指数退避重试

typescript
async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 1000
): Promise<T> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn()
    } catch (error: any) {
      if (attempt === maxRetries) throw error

      // 判断是否值得重试
      const isRetryable =
        error.message?.includes('rate_limit') ||
        error.message?.includes('timeout') ||
        error.message?.includes('overloaded') ||
        error.status === 429 ||
        error.status === 503

      if (!isRetryable) throw error // 非临时性错误,不重试

      const delay = baseDelay * Math.pow(2, attempt) // 1s, 2s, 4s
      console.log(`⏳ 第 ${attempt + 1} 次重试,等待 ${delay}ms...`)
      await new Promise(r => setTimeout(r, delay))
    }
  }
  throw new Error('不可达')
}

// 使用
const result = await withRetry(() =>
  generateText({ model, prompt: '...' })
)

超时控制

typescript
async function withTimeout<T>(
  fn: () => Promise<T>,
  timeoutMs: number
): Promise<T> {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), timeoutMs)

  try {
    return await fn()
  } finally {
    clearTimeout(timer)
  }
}

// 限制单次调用不超过 30 秒
const result = await withTimeout(
  () => generateText({
    model,
    prompt: '...',
    abortSignal: controller.signal,
  }),
  30_000
)

Fallback 机制

重试解决不了的问题(比如某个模型持续不可用),需要 Fallback。

模型级 Fallback

typescript
async function generateWithFallback(prompt: string) {
  const models = [
    model,              // 首选
    modelFallback,      // 备选:更快更便宜
  ]

  for (const model of models) {
    try {
      return await withRetry(() =>
        generateText({ model, prompt }),
        2 // 每个模型最多重试 2 次
      )
    } catch (error) {
      console.warn(`模型 ${model.modelId} 失败,切换下一个`)
    }
  }

  // 所有模型都失败
  return { text: '抱歉,服务暂时不可用,请稍后再试。' }
}

在当前主线 basic/project 中,Fallback 通过完整的 FALLBACK_* 配置启用:

  • 最少只需要配置 FALLBACK_MODEL
  • 没有单独配置的 FALLBACK_PROVIDER / FALLBACK_API_KEY / FALLBACK_BASE_URL 会默认复用主模型对应的 CHAT_* 配置
  • 如果主模型调用失败,或主模型输出通过不了质量检查,再切到 fallback 模型

本课独立 Demo 也支持同前缀配置,最少只需要 FALLBACK_MODEL;如果你还配置了 FALLBACK_API_KEY / FALLBACK_BASE_URL,会优先使用这些值,否则继续复用主模型配置。

功能级 Fallback

typescript
// 正常模式:RAG + LLM
// 降级模式:纯文本检索
async function answerQuestion(question: string) {
  try {
    // 正常:向量检索 + LLM 生成
    const docs = await vectorSearch(question)
    return await generateAnswer(question, docs)
  } catch {
    // 降级:关键词搜索 + 返回原文
    const docs = await keywordSearch(question)
    return {
      text: `以下是相关文档(AI 摘要不可用):\n${docs.map(d => d.content).join('\n---\n')}`,
      degraded: true,
    }
  }
}

Prompt 质量控制技巧

很多质量问题的根源是 Prompt 写得不好。几个实用技巧:

1. 要求模型自我检查

typescript
const system = `你是技术顾问。回答用户问题时:
1. 先回答
2. 然后检查自己的回答是否有以下问题:
   - 是否包含不确定的信息?如果有,标注"[未验证]"
   - 是否遗漏了用户问题的某个方面?
3. 如果不确定答案,直接说"我不确定"`

2. 限制回答范围

typescript
const system = `你只回答以下领域的问题:前端开发、TypeScript、React。
如果用户问的不在这些领域,回复"这个问题超出了我的知识范围"。
不要尝试回答你不擅长的问题。`

3. 幻觉防控

typescript
// 结合 RAG 时,限制模型只从检索结果中回答
const system = `基于以下参考资料回答问题。
规则:
- 只使用参考资料中的信息
- 如果参考资料中没有相关内容,回复"根据现有资料无法回答"
- 不要编造参考资料中没有的信息
- 引用时标注来源编号

<references>
${docs.map((d, i) => `[${i + 1}] ${d.content}`).join('\n')}
</references>`

Human-in-the-Loop

有些场景需要人工介入:高风险操作、低置信度结果、敏感内容。

这里要特别注意:不是所有工具都要审批。 Human-in-the-Loop 主要留给删除数据、发送消息、付款这类高风险动作,或者低置信度又不能自动降级的结果。

CLI 实现:在工具内部询问用户

在 Node.js CLI 程序里,在工具的 execute 内用 readline 向用户确认:

typescript
import * as readline from 'node:readline'
import { generateText, tool } from 'ai'
import { z } from 'zod'

async function confirm(prompt: string): Promise<boolean> {
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
  return new Promise(resolve => {
    rl.question(`${prompt} (y/N) `, answer => {
      rl.close()
      resolve(answer.toLowerCase() === 'y')
    })
  })
}

const result = await generateText({
  model,
  prompt: '删除 doc-001 这个文档',
  tools: {
    // 高风险:删除前询问用户
    deleteDocument: tool({
      description: '从知识库删除文档',
      inputSchema: z.object({ docId: z.string() }),
      execute: async ({ docId }) => {
        const ok = await confirm(`⚠️  即将删除文档 ${docId},确认吗?`)
        if (!ok) return '操作已取消'
        // 实际删除逻辑
        return `已删除文档 ${docId}`
      },
    }),

    // 低风险:直接执行,不询问
    searchDocs: tool({
      description: '搜索文档',
      inputSchema: z.object({ query: z.string() }),
      execute: async ({ query }) => { /* 搜索逻辑 */ },
    }),
  },
})

常见触发条件

操作是否需要确认
读取 / 搜索数据
新增数据视情况
修改 / 删除数据
发送消息 / 付款

Web 应用:用 AI SDK 原生支持

Web 项目中,AI SDK 提供了 needsApproval 属性,不需要手写确认逻辑:

typescript
deleteDocument: tool({
  description: '从知识库删除文档',
  inputSchema: z.object({ docId: z.string() }),
  needsApproval: true,  // 或 async (input) => 按条件决定
  execute: async ({ docId }) => { /* 删除逻辑 */ },
})

客户端用 addToolApprovalResponse 响应,SDK 自动处理暂停与恢复。详见 AI SDK 文档

本课产物

  • ✅ 了解大模型应用的常见质量问题
  • ✅ 掌握 Zod Schema 输出校验 + 二次验证
  • ✅ 掌握指数退避重试和超时控制
  • ✅ 理解 Fallback 机制(模型级 + 功能级)
  • ✅ 了解 Human-in-the-Loop 的设计方式

完整代码在 basic/examples/08-production/01-quality/index.ts

试试看

bash
cd daqi-ai-agent
pnpm exec tsx basic/examples/08-production/01-quality/index.ts
  1. 运行 Demo,先提一个正常问题,确认程序能返回结构化结果
  2. 连续提两个问题,观察输出中的 answer / confidence / reasoning / caveats 以及最后的质量检查结果
  3. 输入 hitl,确认删除工具在真正执行前会先询问你
  4. 如果你配置了 FALLBACK_MODEL,可以把主模型暂时改成不可用模型名,观察程序是否切到 fallback;未配置则跳过这一步

面试追问

Q:如何减少大模型的幻觉?

多管齐下:1. 用 RAG 提供参考资料,并在 Prompt 中限制"只从参考资料回答"。2. 用低 temperature(0-0.3)降低随机性。3. 要求模型标注不确定信息,置信度低时拒绝回答。4. 输出后二次验证(事实核查、来源检查)。

Q:模型调用失败怎么处理?

分三层:1. 重试——指数退避,区分可重试错误(限流、超时)和不可重试错误。2. Fallback——首选模型不可用时切换备选模型。3. 降级——AI 功能完全不可用时,回退到传统逻辑(如关键词搜索替代语义搜索)。

Q:什么场景需要 Human-in-the-Loop?

高风险操作(删除数据、发送消息、付款)、低置信度结果、敏感内容处理。AI SDK 内置 needsApproval,在 Tool 定义时声明即可——可以是静态 true,也可以是按参数动态判断的函数。SDK 负责暂停执行、收集审批响应、再决定是否调用 execute,不需要手写审批流程。

面向前端工程师和独立开发者的 AI 应用工程课程