Agent 输出质量保障:格式控制、校验机制与自动重试策略

摘要

在 AI Agent 的工程化实践中,输出质量保障是决定系统能否真正落地生产环境的核心环节。本文从格式不稳定、内容幻觉、工具调用错误三大典型问题出发,系统性地构建了一套覆盖"事前约束---事中校验---事后重试"的完整质量保障框架。内容涵盖 JSON Mode 与 Structured Output 的底层原理与工程实践、基于 JSON Schema 的多级校验机制、指数退避重试与上下文动态调整策略、在线与离线评估方法论,并提供了一个可直接落地的 Python 实现 Pipeline。适用于已完成 Agent 基础架构搭建、希望提升输出可靠性的工程团队。

版本声明: 本文基于 OpenAI GPT-4o API(2024.08+)、Python 3.11+、Pydantic v2 编写,部分方案兼容 Claude 3.5 Sonnet 及其他主流大模型。不同厂商 API 能力存在差异,文中会标注适用范围。

适用边界: 本文聚焦 Agent 文本输出质量保障,不涉及模型训练阶段的对齐优化、RLHF 等技术路线。所有方案均基于 API 调用层面,不要求模型微调权限。

文章目录

    • 摘要
    • [一、Agent 输出质量的三大问题:格式不稳定、内容幻觉、工具调用错误](#一、Agent 输出质量的三大问题:格式不稳定、内容幻觉、工具调用错误)
      • [1.1 问题全景:为什么 Agent 输出总是"不靠谱"?](#1.1 问题全景:为什么 Agent 输出总是"不靠谱"?)
      • [1.2 格式不稳定:最常见但最致命](#1.2 格式不稳定:最常见但最致命)
      • [1.3 内容幻觉:隐蔽性最强](#1.3 内容幻觉:隐蔽性最强)
      • [1.4 工具调用错误:链路最复杂](#1.4 工具调用错误:链路最复杂)
    • [二、格式控制:JSON Mode、Structured Output、Schema 约束](#二、格式控制:JSON Mode、Structured Output、Schema 约束)
      • [2.1 从自然语言到结构化输出:Agent 的格式演进史](#2.1 从自然语言到结构化输出:Agent 的格式演进史)
      • [2.2 Prompt 约束:最基础但不可忽视](#2.2 Prompt 约束:最基础但不可忽视)
    • 注意事项
      • [2.4 Structured Output:Schema 级别的格式保证](#2.4 Structured Output:Schema 级别的格式保证)
      • [2.5 不同模型的格式控制能力对比](#2.5 不同模型的格式控制能力对比)
      • [2.6 Schema 约束的最佳实践](#2.6 Schema 约束的最佳实践)
    • [三、校验机制:输出 Schema 验证、内容审查、事实核查](#三、校验机制:输出 Schema 验证、内容审查、事实核查)
      • [3.1 校验机制的分层架构](#3.1 校验机制的分层架构)
      • [3.2 第一层:格式校验](#3.2 第一层:格式校验)
      • [3.3 第二层:内容审查](#3.3 第二层:内容审查)
      • [3.4 第三层:事实核查与幻觉检测](#3.4 第三层:事实核查与幻觉检测)
      • [3.5 校验机制的工程化考虑](#3.5 校验机制的工程化考虑)
    • 四、自动重试策略:指数退避、上下文调整、模型切换
      • [4.1 为什么要"自动重试"而不是"直接失败"?](#4.1 为什么要"自动重试"而不是"直接失败"?)
      • [4.2 重试策略的决策树](#4.2 重试策略的决策树)
      • [4.3 指数退避重试](#4.3 指数退避重试)
      • [4.4 上下文调整策略](#4.4 上下文调整策略)
      • [4.5 模型切换策略](#4.5 模型切换策略)
    • 五、质量评估:在线评估指标与离线评估方法
      • [5.1 为什么需要质量评估?](#5.1 为什么需要质量评估?)
      • [5.2 在线评估指标](#5.2 在线评估指标)
      • [5.3 离线评估方法](#5.3 离线评估方法)
    • [六、实战:一个完整的输出质量保障 Pipeline](#六、实战:一个完整的输出质量保障 Pipeline)
      • [6.1 Pipeline 架构设计](#6.1 Pipeline 架构设计)
      • [6.2 Pipeline 的扩展性设计](#6.2 Pipeline 的扩展性设计)
      • [6.3 生产环境部署建议](#6.3 生产环境部署建议)
    • 七、适用边界与风险提示
      • [7.1 方案适用场景](#7.1 方案适用场景)
      • [7.2 方案局限与风险](#7.2 方案局限与风险)
    • 八、总结
    • 参考资料

一、Agent 输出质量的三大问题:格式不稳定、内容幻觉、工具调用错误

1.1 问题全景:为什么 Agent 输出总是"不靠谱"?

如果你在生产环境中跑过 AI Agent,大概率经历过以下场景:

  • Agent 返回了一堆自然语言,但你期望的是结构化 JSON,下游解析直接崩溃
  • Agent 编造了一个不存在的 API 端点,然后"成功"调用了它,返回了虚构的数据
  • Agent 的工具调用参数缺了两个字段,工具执行报错,Agent 却继续往下走
  • 同样一个 Prompt,第一次跑出正确结果,第二次格式就变了,第三次直接超时

这三个问题分别对应 Agent 输出质量的核心三大类挑战:
#mermaid-svg-b0yAiJWvcSKua8FI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-b0yAiJWvcSKua8FI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-b0yAiJWvcSKua8FI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-b0yAiJWvcSKua8FI .error-icon{fill:#552222;}#mermaid-svg-b0yAiJWvcSKua8FI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-b0yAiJWvcSKua8FI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-b0yAiJWvcSKua8FI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-b0yAiJWvcSKua8FI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-b0yAiJWvcSKua8FI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-b0yAiJWvcSKua8FI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-b0yAiJWvcSKua8FI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-b0yAiJWvcSKua8FI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-b0yAiJWvcSKua8FI .marker.cross{stroke:#333333;}#mermaid-svg-b0yAiJWvcSKua8FI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-b0yAiJWvcSKua8FI p{margin:0;}#mermaid-svg-b0yAiJWvcSKua8FI .edge{stroke-width:3;}#mermaid-svg-b0yAiJWvcSKua8FI .section--1 rect,#mermaid-svg-b0yAiJWvcSKua8FI .section--1 path,#mermaid-svg-b0yAiJWvcSKua8FI .section--1 circle,#mermaid-svg-b0yAiJWvcSKua8FI .section--1 polygon,#mermaid-svg-b0yAiJWvcSKua8FI .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .section--1 text{fill:#ffffff;}#mermaid-svg-b0yAiJWvcSKua8FI .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-b0yAiJWvcSKua8FI .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .edge-depth--1{stroke-width:17;}#mermaid-svg-b0yAiJWvcSKua8FI .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled,#mermaid-svg-b0yAiJWvcSKua8FI .disabled circle,#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:lightgray;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:#efefef;}#mermaid-svg-b0yAiJWvcSKua8FI .section-0 rect,#mermaid-svg-b0yAiJWvcSKua8FI .section-0 path,#mermaid-svg-b0yAiJWvcSKua8FI .section-0 circle,#mermaid-svg-b0yAiJWvcSKua8FI .section-0 polygon,#mermaid-svg-b0yAiJWvcSKua8FI .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-b0yAiJWvcSKua8FI .section-0 text{fill:black;}#mermaid-svg-b0yAiJWvcSKua8FI .node-icon-0{font-size:40px;color:black;}#mermaid-svg-b0yAiJWvcSKua8FI .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-b0yAiJWvcSKua8FI .edge-depth-0{stroke-width:14;}#mermaid-svg-b0yAiJWvcSKua8FI .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled,#mermaid-svg-b0yAiJWvcSKua8FI .disabled circle,#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:lightgray;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:#efefef;}#mermaid-svg-b0yAiJWvcSKua8FI .section-1 rect,#mermaid-svg-b0yAiJWvcSKua8FI .section-1 path,#mermaid-svg-b0yAiJWvcSKua8FI .section-1 circle,#mermaid-svg-b0yAiJWvcSKua8FI .section-1 polygon,#mermaid-svg-b0yAiJWvcSKua8FI .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .section-1 text{fill:black;}#mermaid-svg-b0yAiJWvcSKua8FI .node-icon-1{font-size:40px;color:black;}#mermaid-svg-b0yAiJWvcSKua8FI .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .edge-depth-1{stroke-width:11;}#mermaid-svg-b0yAiJWvcSKua8FI .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled,#mermaid-svg-b0yAiJWvcSKua8FI .disabled circle,#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:lightgray;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:#efefef;}#mermaid-svg-b0yAiJWvcSKua8FI .section-2 rect,#mermaid-svg-b0yAiJWvcSKua8FI .section-2 path,#mermaid-svg-b0yAiJWvcSKua8FI .section-2 circle,#mermaid-svg-b0yAiJWvcSKua8FI .section-2 polygon,#mermaid-svg-b0yAiJWvcSKua8FI .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .section-2 text{fill:#ffffff;}#mermaid-svg-b0yAiJWvcSKua8FI .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-b0yAiJWvcSKua8FI .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .edge-depth-2{stroke-width:8;}#mermaid-svg-b0yAiJWvcSKua8FI .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled,#mermaid-svg-b0yAiJWvcSKua8FI .disabled circle,#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:lightgray;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:#efefef;}#mermaid-svg-b0yAiJWvcSKua8FI .section-3 rect,#mermaid-svg-b0yAiJWvcSKua8FI .section-3 path,#mermaid-svg-b0yAiJWvcSKua8FI .section-3 circle,#mermaid-svg-b0yAiJWvcSKua8FI .section-3 polygon,#mermaid-svg-b0yAiJWvcSKua8FI .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .section-3 text{fill:black;}#mermaid-svg-b0yAiJWvcSKua8FI .node-icon-3{font-size:40px;color:black;}#mermaid-svg-b0yAiJWvcSKua8FI .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .edge-depth-3{stroke-width:5;}#mermaid-svg-b0yAiJWvcSKua8FI .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled,#mermaid-svg-b0yAiJWvcSKua8FI .disabled circle,#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:lightgray;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:#efefef;}#mermaid-svg-b0yAiJWvcSKua8FI .section-4 rect,#mermaid-svg-b0yAiJWvcSKua8FI .section-4 path,#mermaid-svg-b0yAiJWvcSKua8FI .section-4 circle,#mermaid-svg-b0yAiJWvcSKua8FI .section-4 polygon,#mermaid-svg-b0yAiJWvcSKua8FI .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .section-4 text{fill:black;}#mermaid-svg-b0yAiJWvcSKua8FI .node-icon-4{font-size:40px;color:black;}#mermaid-svg-b0yAiJWvcSKua8FI .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .edge-depth-4{stroke-width:2;}#mermaid-svg-b0yAiJWvcSKua8FI .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled,#mermaid-svg-b0yAiJWvcSKua8FI .disabled circle,#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:lightgray;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:#efefef;}#mermaid-svg-b0yAiJWvcSKua8FI .section-5 rect,#mermaid-svg-b0yAiJWvcSKua8FI .section-5 path,#mermaid-svg-b0yAiJWvcSKua8FI .section-5 circle,#mermaid-svg-b0yAiJWvcSKua8FI .section-5 polygon,#mermaid-svg-b0yAiJWvcSKua8FI .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .section-5 text{fill:black;}#mermaid-svg-b0yAiJWvcSKua8FI .node-icon-5{font-size:40px;color:black;}#mermaid-svg-b0yAiJWvcSKua8FI .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .edge-depth-5{stroke-width:-1;}#mermaid-svg-b0yAiJWvcSKua8FI .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled,#mermaid-svg-b0yAiJWvcSKua8FI .disabled circle,#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:lightgray;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:#efefef;}#mermaid-svg-b0yAiJWvcSKua8FI .section-6 rect,#mermaid-svg-b0yAiJWvcSKua8FI .section-6 path,#mermaid-svg-b0yAiJWvcSKua8FI .section-6 circle,#mermaid-svg-b0yAiJWvcSKua8FI .section-6 polygon,#mermaid-svg-b0yAiJWvcSKua8FI .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .section-6 text{fill:black;}#mermaid-svg-b0yAiJWvcSKua8FI .node-icon-6{font-size:40px;color:black;}#mermaid-svg-b0yAiJWvcSKua8FI .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .edge-depth-6{stroke-width:-4;}#mermaid-svg-b0yAiJWvcSKua8FI .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled,#mermaid-svg-b0yAiJWvcSKua8FI .disabled circle,#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:lightgray;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:#efefef;}#mermaid-svg-b0yAiJWvcSKua8FI .section-7 rect,#mermaid-svg-b0yAiJWvcSKua8FI .section-7 path,#mermaid-svg-b0yAiJWvcSKua8FI .section-7 circle,#mermaid-svg-b0yAiJWvcSKua8FI .section-7 polygon,#mermaid-svg-b0yAiJWvcSKua8FI .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .section-7 text{fill:black;}#mermaid-svg-b0yAiJWvcSKua8FI .node-icon-7{font-size:40px;color:black;}#mermaid-svg-b0yAiJWvcSKua8FI .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .edge-depth-7{stroke-width:-7;}#mermaid-svg-b0yAiJWvcSKua8FI .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled,#mermaid-svg-b0yAiJWvcSKua8FI .disabled circle,#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:lightgray;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:#efefef;}#mermaid-svg-b0yAiJWvcSKua8FI .section-8 rect,#mermaid-svg-b0yAiJWvcSKua8FI .section-8 path,#mermaid-svg-b0yAiJWvcSKua8FI .section-8 circle,#mermaid-svg-b0yAiJWvcSKua8FI .section-8 polygon,#mermaid-svg-b0yAiJWvcSKua8FI .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .section-8 text{fill:black;}#mermaid-svg-b0yAiJWvcSKua8FI .node-icon-8{font-size:40px;color:black;}#mermaid-svg-b0yAiJWvcSKua8FI .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .edge-depth-8{stroke-width:-10;}#mermaid-svg-b0yAiJWvcSKua8FI .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled,#mermaid-svg-b0yAiJWvcSKua8FI .disabled circle,#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:lightgray;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:#efefef;}#mermaid-svg-b0yAiJWvcSKua8FI .section-9 rect,#mermaid-svg-b0yAiJWvcSKua8FI .section-9 path,#mermaid-svg-b0yAiJWvcSKua8FI .section-9 circle,#mermaid-svg-b0yAiJWvcSKua8FI .section-9 polygon,#mermaid-svg-b0yAiJWvcSKua8FI .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .section-9 text{fill:black;}#mermaid-svg-b0yAiJWvcSKua8FI .node-icon-9{font-size:40px;color:black;}#mermaid-svg-b0yAiJWvcSKua8FI .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .edge-depth-9{stroke-width:-13;}#mermaid-svg-b0yAiJWvcSKua8FI .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled,#mermaid-svg-b0yAiJWvcSKua8FI .disabled circle,#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:lightgray;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:#efefef;}#mermaid-svg-b0yAiJWvcSKua8FI .section-10 rect,#mermaid-svg-b0yAiJWvcSKua8FI .section-10 path,#mermaid-svg-b0yAiJWvcSKua8FI .section-10 circle,#mermaid-svg-b0yAiJWvcSKua8FI .section-10 polygon,#mermaid-svg-b0yAiJWvcSKua8FI .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .section-10 text{fill:black;}#mermaid-svg-b0yAiJWvcSKua8FI .node-icon-10{font-size:40px;color:black;}#mermaid-svg-b0yAiJWvcSKua8FI .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .edge-depth-10{stroke-width:-16;}#mermaid-svg-b0yAiJWvcSKua8FI .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled,#mermaid-svg-b0yAiJWvcSKua8FI .disabled circle,#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:lightgray;}#mermaid-svg-b0yAiJWvcSKua8FI .disabled text{fill:#efefef;}#mermaid-svg-b0yAiJWvcSKua8FI .section-root rect,#mermaid-svg-b0yAiJWvcSKua8FI .section-root path,#mermaid-svg-b0yAiJWvcSKua8FI .section-root circle,#mermaid-svg-b0yAiJWvcSKua8FI .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-b0yAiJWvcSKua8FI .section-root text{fill:#ffffff;}#mermaid-svg-b0yAiJWvcSKua8FI .section-root span{color:#ffffff;}#mermaid-svg-b0yAiJWvcSKua8FI .section-2 span{color:#ffffff;}#mermaid-svg-b0yAiJWvcSKua8FI .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-b0yAiJWvcSKua8FI .edge{fill:none;}#mermaid-svg-b0yAiJWvcSKua8FI .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-b0yAiJWvcSKua8FI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Agent输出质量问题
格式不稳定
JSON结构漂移
字段缺失
类型不一致
编码与转义错误
内容幻觉
事实性幻觉
工具幻觉
引用幻觉
工具调用错误
参数缺失
参数类型错误
调用顺序错误
幻想不存在的工具

1.2 格式不稳定:最常见但最致命

图:Agent 输出质量三大问题(格式不稳定、内容幻觉、工具调用错误)及质量保障 Pipeline 全景

格式不稳定是最常见的输出质量问题,也是最容易造成系统级故障的问题。当你的下游系统依赖 Agent 输出的 JSON 进行解析时,任何一个格式偏差都可能导致 JSON.parse() 抛出异常,整个请求链路中断。

典型表现:

问题类型 示例 根因
JSON 结构漂移 期望 {code, message, data},返回 {data, code, msg} Prompt 未做 Schema 约束
字段缺失 期望返回 5 个字段,只返回了 3 个 模型理解偏差或 token 截断
类型不一致 期望 code 为 integer,返回 "200" 未启用 Structured Output
编码错误 中文未转义或 Unicode 转义格式混乱 模型 Tokenizer 与编码器不一致
混合输出 JSON 前后混入自然语言解释 Prompt 设计不当

1.3 内容幻觉:隐蔽性最强

幻觉问题的危险之处在于:它不会导致程序报错,但会传播错误信息。Agent 可能返回格式完美的 JSON,但里面的内容是编造的。这在 Agent 调用外部工具的场景中尤其严重------Agent 可能"编造"一个 API 调用结果,而实际上从未发起过调用。
#mermaid-svg-n1lybl5xC4MoSLcE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-n1lybl5xC4MoSLcE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-n1lybl5xC4MoSLcE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-n1lybl5xC4MoSLcE .error-icon{fill:#552222;}#mermaid-svg-n1lybl5xC4MoSLcE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-n1lybl5xC4MoSLcE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-n1lybl5xC4MoSLcE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-n1lybl5xC4MoSLcE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-n1lybl5xC4MoSLcE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-n1lybl5xC4MoSLcE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-n1lybl5xC4MoSLcE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-n1lybl5xC4MoSLcE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-n1lybl5xC4MoSLcE .marker.cross{stroke:#333333;}#mermaid-svg-n1lybl5xC4MoSLcE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-n1lybl5xC4MoSLcE p{margin:0;}#mermaid-svg-n1lybl5xC4MoSLcE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-n1lybl5xC4MoSLcE .cluster-label text{fill:#333;}#mermaid-svg-n1lybl5xC4MoSLcE .cluster-label span{color:#333;}#mermaid-svg-n1lybl5xC4MoSLcE .cluster-label span p{background-color:transparent;}#mermaid-svg-n1lybl5xC4MoSLcE .label text,#mermaid-svg-n1lybl5xC4MoSLcE span{fill:#333;color:#333;}#mermaid-svg-n1lybl5xC4MoSLcE .node rect,#mermaid-svg-n1lybl5xC4MoSLcE .node circle,#mermaid-svg-n1lybl5xC4MoSLcE .node ellipse,#mermaid-svg-n1lybl5xC4MoSLcE .node polygon,#mermaid-svg-n1lybl5xC4MoSLcE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-n1lybl5xC4MoSLcE .rough-node .label text,#mermaid-svg-n1lybl5xC4MoSLcE .node .label text,#mermaid-svg-n1lybl5xC4MoSLcE .image-shape .label,#mermaid-svg-n1lybl5xC4MoSLcE .icon-shape .label{text-anchor:middle;}#mermaid-svg-n1lybl5xC4MoSLcE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-n1lybl5xC4MoSLcE .rough-node .label,#mermaid-svg-n1lybl5xC4MoSLcE .node .label,#mermaid-svg-n1lybl5xC4MoSLcE .image-shape .label,#mermaid-svg-n1lybl5xC4MoSLcE .icon-shape .label{text-align:center;}#mermaid-svg-n1lybl5xC4MoSLcE .node.clickable{cursor:pointer;}#mermaid-svg-n1lybl5xC4MoSLcE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-n1lybl5xC4MoSLcE .arrowheadPath{fill:#333333;}#mermaid-svg-n1lybl5xC4MoSLcE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-n1lybl5xC4MoSLcE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-n1lybl5xC4MoSLcE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-n1lybl5xC4MoSLcE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-n1lybl5xC4MoSLcE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-n1lybl5xC4MoSLcE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-n1lybl5xC4MoSLcE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-n1lybl5xC4MoSLcE .cluster text{fill:#333;}#mermaid-svg-n1lybl5xC4MoSLcE .cluster span{color:#333;}#mermaid-svg-n1lybl5xC4MoSLcE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-n1lybl5xC4MoSLcE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-n1lybl5xC4MoSLcE rect.text{fill:none;stroke-width:0;}#mermaid-svg-n1lybl5xC4MoSLcE .icon-shape,#mermaid-svg-n1lybl5xC4MoSLcE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-n1lybl5xC4MoSLcE .icon-shape p,#mermaid-svg-n1lybl5xC4MoSLcE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-n1lybl5xC4MoSLcE .icon-shape .label rect,#mermaid-svg-n1lybl5xC4MoSLcE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-n1lybl5xC4MoSLcE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-n1lybl5xC4MoSLcE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-n1lybl5xC4MoSLcE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

通过
未通过
Agent 接收任务
是否需要调用工具?
调用工具 API
直接生成回答
获取真实结果
基于真实结果生成回答
基于模型知识生成回答
输出
幻觉检测
✅ 质量合格
❌ 触发重试/人工介入

1.4 工具调用错误:链路最复杂

当 Agent 需要调用外部工具时,输出质量不仅涉及文本格式,还涉及调用参数的完整性、类型正确性和调用顺序的合理性。这是 Agent 系统中最复杂的质量问题,因为它跨越了"文本生成"和"工具执行"两个领域。

常见的工具调用错误包括:

  1. 参数缺失:Agent 在调用工具时省略了必填参数
  2. 参数类型错误:期望传入 integer,实际传入 string
  3. 调用顺序错误:先调用需要认证的 API,但还没调用登录接口
  4. 工具幻觉:调用了一个不存在的工具或调用了错误的工具版本
  5. 结果误读:工具返回了错误码,Agent 却当作成功结果继续处理

这三大类问题不是孤立存在的,它们经常交叉出现:格式不稳定导致工具参数解析失败,内容幻觉导致工具调用参数编造,工具调用错误又可能引发新的格式问题。因此,质量保障必须是一个系统性工程,而非单点修复。


二、格式控制:JSON Mode、Structured Output、Schema 约束

2.1 从自然语言到结构化输出:Agent 的格式演进史

在大模型发展的早期阶段,开发者唯一能做的就是写一段精心设计的 Prompt,然后在后面加一句"请以 JSON 格式返回"。这种方法的效果完全取决于模型------同一套 Prompt,跑十次可能出八种不同的 JSON 结构。

随着 API 能力的演进,格式控制经历了三个阶段:
#mermaid-svg-FNBpub11yiWz3gkW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FNBpub11yiWz3gkW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FNBpub11yiWz3gkW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FNBpub11yiWz3gkW .error-icon{fill:#552222;}#mermaid-svg-FNBpub11yiWz3gkW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FNBpub11yiWz3gkW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FNBpub11yiWz3gkW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FNBpub11yiWz3gkW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FNBpub11yiWz3gkW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FNBpub11yiWz3gkW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FNBpub11yiWz3gkW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FNBpub11yiWz3gkW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FNBpub11yiWz3gkW .marker.cross{stroke:#333333;}#mermaid-svg-FNBpub11yiWz3gkW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FNBpub11yiWz3gkW p{margin:0;}#mermaid-svg-FNBpub11yiWz3gkW .edge{stroke-width:3;}#mermaid-svg-FNBpub11yiWz3gkW .section--1 rect,#mermaid-svg-FNBpub11yiWz3gkW .section--1 path,#mermaid-svg-FNBpub11yiWz3gkW .section--1 circle,#mermaid-svg-FNBpub11yiWz3gkW .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .section--1 text{fill:#ffffff;}#mermaid-svg-FNBpub11yiWz3gkW .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-FNBpub11yiWz3gkW .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .edge-depth--1{stroke-width:17;}#mermaid-svg-FNBpub11yiWz3gkW .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-FNBpub11yiWz3gkW .lineWrapper line{stroke:#ffffff;}#mermaid-svg-FNBpub11yiWz3gkW .disabled,#mermaid-svg-FNBpub11yiWz3gkW .disabled circle,#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:lightgray;}#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:#efefef;}#mermaid-svg-FNBpub11yiWz3gkW .section-0 rect,#mermaid-svg-FNBpub11yiWz3gkW .section-0 path,#mermaid-svg-FNBpub11yiWz3gkW .section-0 circle,#mermaid-svg-FNBpub11yiWz3gkW .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-FNBpub11yiWz3gkW .section-0 text{fill:black;}#mermaid-svg-FNBpub11yiWz3gkW .node-icon-0{font-size:40px;color:black;}#mermaid-svg-FNBpub11yiWz3gkW .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-FNBpub11yiWz3gkW .edge-depth-0{stroke-width:14;}#mermaid-svg-FNBpub11yiWz3gkW .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-FNBpub11yiWz3gkW .lineWrapper line{stroke:black;}#mermaid-svg-FNBpub11yiWz3gkW .disabled,#mermaid-svg-FNBpub11yiWz3gkW .disabled circle,#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:lightgray;}#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:#efefef;}#mermaid-svg-FNBpub11yiWz3gkW .section-1 rect,#mermaid-svg-FNBpub11yiWz3gkW .section-1 path,#mermaid-svg-FNBpub11yiWz3gkW .section-1 circle,#mermaid-svg-FNBpub11yiWz3gkW .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .section-1 text{fill:black;}#mermaid-svg-FNBpub11yiWz3gkW .node-icon-1{font-size:40px;color:black;}#mermaid-svg-FNBpub11yiWz3gkW .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .edge-depth-1{stroke-width:11;}#mermaid-svg-FNBpub11yiWz3gkW .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-FNBpub11yiWz3gkW .lineWrapper line{stroke:black;}#mermaid-svg-FNBpub11yiWz3gkW .disabled,#mermaid-svg-FNBpub11yiWz3gkW .disabled circle,#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:lightgray;}#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:#efefef;}#mermaid-svg-FNBpub11yiWz3gkW .section-2 rect,#mermaid-svg-FNBpub11yiWz3gkW .section-2 path,#mermaid-svg-FNBpub11yiWz3gkW .section-2 circle,#mermaid-svg-FNBpub11yiWz3gkW .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .section-2 text{fill:#ffffff;}#mermaid-svg-FNBpub11yiWz3gkW .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-FNBpub11yiWz3gkW .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .edge-depth-2{stroke-width:8;}#mermaid-svg-FNBpub11yiWz3gkW .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-FNBpub11yiWz3gkW .lineWrapper line{stroke:#ffffff;}#mermaid-svg-FNBpub11yiWz3gkW .disabled,#mermaid-svg-FNBpub11yiWz3gkW .disabled circle,#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:lightgray;}#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:#efefef;}#mermaid-svg-FNBpub11yiWz3gkW .section-3 rect,#mermaid-svg-FNBpub11yiWz3gkW .section-3 path,#mermaid-svg-FNBpub11yiWz3gkW .section-3 circle,#mermaid-svg-FNBpub11yiWz3gkW .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .section-3 text{fill:black;}#mermaid-svg-FNBpub11yiWz3gkW .node-icon-3{font-size:40px;color:black;}#mermaid-svg-FNBpub11yiWz3gkW .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .edge-depth-3{stroke-width:5;}#mermaid-svg-FNBpub11yiWz3gkW .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-FNBpub11yiWz3gkW .lineWrapper line{stroke:black;}#mermaid-svg-FNBpub11yiWz3gkW .disabled,#mermaid-svg-FNBpub11yiWz3gkW .disabled circle,#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:lightgray;}#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:#efefef;}#mermaid-svg-FNBpub11yiWz3gkW .section-4 rect,#mermaid-svg-FNBpub11yiWz3gkW .section-4 path,#mermaid-svg-FNBpub11yiWz3gkW .section-4 circle,#mermaid-svg-FNBpub11yiWz3gkW .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .section-4 text{fill:black;}#mermaid-svg-FNBpub11yiWz3gkW .node-icon-4{font-size:40px;color:black;}#mermaid-svg-FNBpub11yiWz3gkW .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .edge-depth-4{stroke-width:2;}#mermaid-svg-FNBpub11yiWz3gkW .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-FNBpub11yiWz3gkW .lineWrapper line{stroke:black;}#mermaid-svg-FNBpub11yiWz3gkW .disabled,#mermaid-svg-FNBpub11yiWz3gkW .disabled circle,#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:lightgray;}#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:#efefef;}#mermaid-svg-FNBpub11yiWz3gkW .section-5 rect,#mermaid-svg-FNBpub11yiWz3gkW .section-5 path,#mermaid-svg-FNBpub11yiWz3gkW .section-5 circle,#mermaid-svg-FNBpub11yiWz3gkW .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .section-5 text{fill:black;}#mermaid-svg-FNBpub11yiWz3gkW .node-icon-5{font-size:40px;color:black;}#mermaid-svg-FNBpub11yiWz3gkW .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .edge-depth-5{stroke-width:-1;}#mermaid-svg-FNBpub11yiWz3gkW .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-FNBpub11yiWz3gkW .lineWrapper line{stroke:black;}#mermaid-svg-FNBpub11yiWz3gkW .disabled,#mermaid-svg-FNBpub11yiWz3gkW .disabled circle,#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:lightgray;}#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:#efefef;}#mermaid-svg-FNBpub11yiWz3gkW .section-6 rect,#mermaid-svg-FNBpub11yiWz3gkW .section-6 path,#mermaid-svg-FNBpub11yiWz3gkW .section-6 circle,#mermaid-svg-FNBpub11yiWz3gkW .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .section-6 text{fill:black;}#mermaid-svg-FNBpub11yiWz3gkW .node-icon-6{font-size:40px;color:black;}#mermaid-svg-FNBpub11yiWz3gkW .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .edge-depth-6{stroke-width:-4;}#mermaid-svg-FNBpub11yiWz3gkW .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-FNBpub11yiWz3gkW .lineWrapper line{stroke:black;}#mermaid-svg-FNBpub11yiWz3gkW .disabled,#mermaid-svg-FNBpub11yiWz3gkW .disabled circle,#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:lightgray;}#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:#efefef;}#mermaid-svg-FNBpub11yiWz3gkW .section-7 rect,#mermaid-svg-FNBpub11yiWz3gkW .section-7 path,#mermaid-svg-FNBpub11yiWz3gkW .section-7 circle,#mermaid-svg-FNBpub11yiWz3gkW .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .section-7 text{fill:black;}#mermaid-svg-FNBpub11yiWz3gkW .node-icon-7{font-size:40px;color:black;}#mermaid-svg-FNBpub11yiWz3gkW .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .edge-depth-7{stroke-width:-7;}#mermaid-svg-FNBpub11yiWz3gkW .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-FNBpub11yiWz3gkW .lineWrapper line{stroke:black;}#mermaid-svg-FNBpub11yiWz3gkW .disabled,#mermaid-svg-FNBpub11yiWz3gkW .disabled circle,#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:lightgray;}#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:#efefef;}#mermaid-svg-FNBpub11yiWz3gkW .section-8 rect,#mermaid-svg-FNBpub11yiWz3gkW .section-8 path,#mermaid-svg-FNBpub11yiWz3gkW .section-8 circle,#mermaid-svg-FNBpub11yiWz3gkW .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .section-8 text{fill:black;}#mermaid-svg-FNBpub11yiWz3gkW .node-icon-8{font-size:40px;color:black;}#mermaid-svg-FNBpub11yiWz3gkW .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .edge-depth-8{stroke-width:-10;}#mermaid-svg-FNBpub11yiWz3gkW .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-FNBpub11yiWz3gkW .lineWrapper line{stroke:black;}#mermaid-svg-FNBpub11yiWz3gkW .disabled,#mermaid-svg-FNBpub11yiWz3gkW .disabled circle,#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:lightgray;}#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:#efefef;}#mermaid-svg-FNBpub11yiWz3gkW .section-9 rect,#mermaid-svg-FNBpub11yiWz3gkW .section-9 path,#mermaid-svg-FNBpub11yiWz3gkW .section-9 circle,#mermaid-svg-FNBpub11yiWz3gkW .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .section-9 text{fill:black;}#mermaid-svg-FNBpub11yiWz3gkW .node-icon-9{font-size:40px;color:black;}#mermaid-svg-FNBpub11yiWz3gkW .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .edge-depth-9{stroke-width:-13;}#mermaid-svg-FNBpub11yiWz3gkW .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-FNBpub11yiWz3gkW .lineWrapper line{stroke:black;}#mermaid-svg-FNBpub11yiWz3gkW .disabled,#mermaid-svg-FNBpub11yiWz3gkW .disabled circle,#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:lightgray;}#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:#efefef;}#mermaid-svg-FNBpub11yiWz3gkW .section-10 rect,#mermaid-svg-FNBpub11yiWz3gkW .section-10 path,#mermaid-svg-FNBpub11yiWz3gkW .section-10 circle,#mermaid-svg-FNBpub11yiWz3gkW .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .section-10 text{fill:black;}#mermaid-svg-FNBpub11yiWz3gkW .node-icon-10{font-size:40px;color:black;}#mermaid-svg-FNBpub11yiWz3gkW .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .edge-depth-10{stroke-width:-16;}#mermaid-svg-FNBpub11yiWz3gkW .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-FNBpub11yiWz3gkW .lineWrapper line{stroke:black;}#mermaid-svg-FNBpub11yiWz3gkW .disabled,#mermaid-svg-FNBpub11yiWz3gkW .disabled circle,#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:lightgray;}#mermaid-svg-FNBpub11yiWz3gkW .disabled text{fill:#efefef;}#mermaid-svg-FNBpub11yiWz3gkW .section-root rect,#mermaid-svg-FNBpub11yiWz3gkW .section-root path,#mermaid-svg-FNBpub11yiWz3gkW .section-root circle{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-FNBpub11yiWz3gkW .section-root text{fill:#ffffff;}#mermaid-svg-FNBpub11yiWz3gkW .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-FNBpub11yiWz3gkW .edge{fill:none;}#mermaid-svg-FNBpub11yiWz3gkW .eventWrapper{filter:brightness(120%);}#mermaid-svg-FNBpub11yiWz3gkW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 2023 早期 Prompt 约束时代 "请返回JSON格式" 无保证, 依赖模型理解 2023 中期 JSON Mode 时代 response_format = json_object 保证合法JSON, 不保证Schema 2024 至今 Structured Output 时代 response_format = json_schema 保证合法JSON + Schema约束 Agent 输出格式控制演进

2.2 Prompt 约束:最基础但不可忽视

即使有了 JSON Mode 和 Structured Output,Prompt 层面的格式约束依然重要。它是第一道防线,也是最灵活的一道防线。

python 复制代码
import json
from openai import OpenAI

client = OpenAI()

FORMAT_CONSTRAINT_PROMPT = """你是一个数据提取助手。请严格按照以下JSON格式输出:

## 输出格式
```json
{
  "intent": "用户意图分类,可选值:query | create | update | delete",
  "confidence": "置信度,0-1之间的浮点数,保留2位小数",
  "entities": [
    {
      "type": "实体类型",
      "value": "实体值",
      "start": "在原文中的起始位置,整数",
      "end": "在原文中的结束位置,整数"
    }
  ],
  "raw_input": "用户原始输入文本"
}

注意事项

  1. 只输出JSON,不要输出任何其他内容
  2. 不要在JSON前后添加markdown标记或解释文字
  3. 如果无法提取某个字段,使用null值
  4. 数组字段至少返回一个元素,可为空数组
    """

response = client.chat.completions.create(

model="gpt-4o",

messages=[

{"role": "system", "content": FORMAT_CONSTRAINT_PROMPT},

{"role": "user", "content": "帮我把张三的邮箱从 zhangsan@test.com 改成 zhangsan@new.com"}

],

temperature=0.1 # 低温度提升格式稳定性

)

result = json.loads(response.choices0.message.content)

print(json.dumps(result, ensure_ascii=False, indent=2))

复制代码
**代码解释:** 上述代码展示了最基础的 Prompt 层面格式控制方案。关键设计点包括:使用具体的 JSON 模板示例而非模糊描述、明确列出每个字段的类型和取值范围、通过 `temperature=0.1` 降低随机性以提升格式稳定性、以及"只输出JSON"的明确指令。这种方式虽然不提供格式保证,但在简单场景下依然是最轻量的选择。

### 2.3 JSON Mode:保证合法性但不保证 Schema

OpenAI 在 2023 年推出了 `response_format={"type": "json_object"}` 的 JSON Mode,这是 API 层面格式控制的第一个正式方案。

```python
# JSON Mode:保证输出是合法JSON,但不保证Schema
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "你是一个天气查询助手。以JSON格式返回天气信息。"},
        {"role": "user", "content": "北京今天天气怎么样?"}
    ],
    response_format={"type": "json_object"},  # 核心参数
    temperature=0
)

# JSON Mode 的保证:
# 1. 输出一定是合法的 JSON,可以被 json.loads() 解析
# 2. 但不保证包含哪些字段、字段类型、字段是否完整

import json
data = json.loads(response.choices[0].message.content)
# data 可能是 {"city": "北京", "weather": "晴"}
# 也可能是 {"location": "北京", "condition": "sunny", "temp": 25}
# 两种都是合法JSON,但结构完全不同

代码解释: JSON Mode 的核心价值在于保证输出的字符串可以被 json.loads() 成功解析,不会再出现"JSON 前后混入自然语言"或"引号未闭合"等语法错误。但它的局限在于不做 Schema 校验------你无法保证输出的 JSON 包含哪些字段、字段的类型是什么。JSON Mode 适合输出结构简单、字段固定的场景。

2.4 Structured Output:Schema 级别的格式保证

2024 年 OpenAI 推出的 Structured Output 是目前最强的格式控制方案。它允许开发者在 API 请求中传入一个 JSON Schema,模型保证输出完全符合该 Schema。

python 复制代码
from pydantic import BaseModel, Field
from openai import OpenAI

client = OpenAI()

class WeatherEntity(BaseModel):
    type: str = Field(description="实体类型,如 city, date, weather_condition")
    value: str = Field(description="实体值")
    confidence: float = Field(description="置信度,0-1", ge=0, le=1)

class WeatherResponse(BaseModel):
    city: str = Field(description="查询的城市名称")
    date: str = Field(description="查询的日期,YYYY-MM-DD格式")
    temperature: int = Field(description="温度,摄氏度")
    weather_condition: str = Field(description="天气状况")
    suggestions: list[str] = Field(description="出行建议列表,至少1条")
    entities: list[WeatherEntity] = Field(description="提取的实体列表")

response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "你是天气查询助手。"},
        {"role": "user", "content": "北京明天天气怎么样?适合出行吗?"}
    ],
    response_format=WeatherResponse,  # 直接传入 Pydantic 模型
    temperature=0
)

result = response.choices[0].message.parsed  # 直接得到 Pydantic 对象
print(f"城市: {result.city}")
print(f"温度: {result.temperature}°C")
print(f"建议: {', '.join(result.suggestions)}")

代码解释: 这是目前最先进的格式控制方案。核心 API 是 client.beta.chat.completions.parse(),直接接受一个 Pydantic 模型作为 response_format 参数。模型在生成时会严格按照该 Schema 输出:所有必填字段必定存在、类型完全匹配、约束条件自动满足。返回结果可以直接通过 .parsed 属性获取 Pydantic 对象,无需手动 json.loads(),也不需要写防御性校验代码。

2.5 不同模型的格式控制能力对比

能力 OpenAI GPT-4o Claude 3.5 Sonnet Gemini 1.5 Pro 通义千问 Max
Prompt 约束
JSON Mode ❌ (需 Prompt)
Structured Output ✅ (JSON Schema) ✅ (Tool Use) ✅ (Schema) ❌ (需 Prompt)
Schema 递归深度 支持 嵌套受限 支持 不支持
数组约束 支持 支持 支持 不支持

选型建议: 如果对格式可靠性要求极高(如金融、医疗场景),优先选择支持 Structured Output 的模型。如果只是简单 JSON 输出,JSON Mode 足矣。对于不支持原生 Schema 约束的模型,可以通过 Pydantic 后校验加重试机制来弥补。

2.6 Schema 约束的最佳实践

1. 字段描述要精确到取值范围

python 复制代码
# ❌ 模糊的描述
class BadSchema(BaseModel):
    status: str = Field(description="状态")

# ✅ 精确的描述
class GoodSchema(BaseModel):
    status: str = Field(
        description="处理状态,必须是以下值之一:pending、processing、success、failed",
        pattern=r"^(pending|processing|success|failed)$"
    )

2. 复杂结构用嵌套模型,不要用扁平结构

python 复制代码
# ❌ 扁平结构,难以维护
class FlatOrder(BaseModel):
    user_name: str
    user_phone: str
    order_id: str
    item_name_1: str
    item_qty_1: int
    item_name_2: str
    item_qty_2: int

# ✅ 嵌套结构,清晰可扩展
class OrderItem(BaseModel):
    name: str = Field(description="商品名称")
    quantity: int = Field(description="数量", ge=1)
    price: float = Field(description="单价", ge=0)

class User(BaseModel):
    name: str = Field(description="用户姓名")
    phone: str = Field(description="手机号", pattern=r"^1[3-9]\d{9}$")

class NestedOrder(BaseModel):
    user: User
    order_id: str = Field(description="订单ID")
    amount: float = Field(description="订单总金额", ge=0)
    items: list[OrderItem] = Field(description="商品列表", min_items=1)

代码解释: 这两段代码对比了 Schema 设计的好坏实践。第一个版本使用扁平结构,所有字段平铺在一个模型中,不仅可读性差,而且难以扩展。第二个版本使用嵌套模型,将用户信息和商品信息分别封装为独立模型,结构清晰且易于扩展。同时注意 Field 参数的使用:pattern 提供正则约束、ge/le 提供数值范围约束、min_items 提供数组长度约束。


三、校验机制:输出 Schema 验证、内容审查、事实核查

3.1 校验机制的分层架构

格式控制解决了"输出是否合法"的问题,但解决不了"输出是否正确"的问题。校验机制是格式控制之后的第二道防线,负责在 Agent 输出生成后、交付下游处理前进行全面检查。
#mermaid-svg-s6po34s4KosbzF70{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-s6po34s4KosbzF70 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-s6po34s4KosbzF70 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-s6po34s4KosbzF70 .error-icon{fill:#552222;}#mermaid-svg-s6po34s4KosbzF70 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-s6po34s4KosbzF70 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-s6po34s4KosbzF70 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-s6po34s4KosbzF70 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-s6po34s4KosbzF70 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-s6po34s4KosbzF70 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-s6po34s4KosbzF70 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-s6po34s4KosbzF70 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-s6po34s4KosbzF70 .marker.cross{stroke:#333333;}#mermaid-svg-s6po34s4KosbzF70 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-s6po34s4KosbzF70 p{margin:0;}#mermaid-svg-s6po34s4KosbzF70 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-s6po34s4KosbzF70 .cluster-label text{fill:#333;}#mermaid-svg-s6po34s4KosbzF70 .cluster-label span{color:#333;}#mermaid-svg-s6po34s4KosbzF70 .cluster-label span p{background-color:transparent;}#mermaid-svg-s6po34s4KosbzF70 .label text,#mermaid-svg-s6po34s4KosbzF70 span{fill:#333;color:#333;}#mermaid-svg-s6po34s4KosbzF70 .node rect,#mermaid-svg-s6po34s4KosbzF70 .node circle,#mermaid-svg-s6po34s4KosbzF70 .node ellipse,#mermaid-svg-s6po34s4KosbzF70 .node polygon,#mermaid-svg-s6po34s4KosbzF70 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-s6po34s4KosbzF70 .rough-node .label text,#mermaid-svg-s6po34s4KosbzF70 .node .label text,#mermaid-svg-s6po34s4KosbzF70 .image-shape .label,#mermaid-svg-s6po34s4KosbzF70 .icon-shape .label{text-anchor:middle;}#mermaid-svg-s6po34s4KosbzF70 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-s6po34s4KosbzF70 .rough-node .label,#mermaid-svg-s6po34s4KosbzF70 .node .label,#mermaid-svg-s6po34s4KosbzF70 .image-shape .label,#mermaid-svg-s6po34s4KosbzF70 .icon-shape .label{text-align:center;}#mermaid-svg-s6po34s4KosbzF70 .node.clickable{cursor:pointer;}#mermaid-svg-s6po34s4KosbzF70 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-s6po34s4KosbzF70 .arrowheadPath{fill:#333333;}#mermaid-svg-s6po34s4KosbzF70 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-s6po34s4KosbzF70 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-s6po34s4KosbzF70 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-s6po34s4KosbzF70 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-s6po34s4KosbzF70 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-s6po34s4KosbzF70 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-s6po34s4KosbzF70 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-s6po34s4KosbzF70 .cluster text{fill:#333;}#mermaid-svg-s6po34s4KosbzF70 .cluster span{color:#333;}#mermaid-svg-s6po34s4KosbzF70 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-s6po34s4KosbzF70 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-s6po34s4KosbzF70 rect.text{fill:none;stroke-width:0;}#mermaid-svg-s6po34s4KosbzF70 .icon-shape,#mermaid-svg-s6po34s4KosbzF70 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-s6po34s4KosbzF70 .icon-shape p,#mermaid-svg-s6po34s4KosbzF70 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-s6po34s4KosbzF70 .icon-shape .label rect,#mermaid-svg-s6po34s4KosbzF70 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-s6po34s4KosbzF70 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-s6po34s4KosbzF70 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-s6po34s4KosbzF70 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第三层:语义校验
第二层:内容校验
第一层:格式校验
Agent 输出


原始输出文本
JSON 解析检查
Schema 验证
类型与约束检查
字段完整性检查
业务规则校验
敏感内容审查
事实核查
一致性检查
幻觉检测
校验通过?
✅ 输出合格
❌ 触发重试机制

3.2 第一层:格式校验

格式校验是最基本的校验层,负责检查输出是否符合预期的结构和类型规则。

python 复制代码
from pydantic import BaseModel, Field, ValidationError
from typing import Optional
import json

class AgentOutputSchema(BaseModel):
    """Agent 输出的标准 Schema"""
    intent: str = Field(
        description="用户意图",
        pattern=r"^(query|create|update|delete)$"
    )
    confidence: float = Field(description="置信度", ge=0.0, le=1.0)
    response: str = Field(description="Agent 回复内容", min_length=10, max_length=2000)
    tool_calls: list[dict] = Field(description="工具调用列表", default_factory=list)
    metadata: Optional[dict] = Field(description="元数据", default=None)

def validate_agent_output(raw_output: str) -> tuple[bool, str, Optional[AgentOutputSchema]]:
    """三步校验 Agent 输出"""
    # Step 1: JSON 解析检查
    try:
        data = json.loads(raw_output)
    except json.JSONDecodeError as e:
        return False, f"JSON解析失败: {e}", None
    
    # Step 2: Schema 验证
    try:
        parsed = AgentOutputSchema(**data)
    except ValidationError as e:
        error_details = []
        for err in e.errors():
            field = ".".join(str(x) for x in err["loc"])
            error_details.append(f"字段[{field}]: {err['msg']}")
        return False, "; ".join(error_details), None
    
    # Step 3: 业务规则校验
    if parsed.confidence < 0.5 and not parsed.metadata:
        return False, "置信度低于0.5但缺少metadata解释", None
    if parsed.tool_calls and parsed.intent == "query":
        return False, "查询意图不应包含工具调用", None
    if parsed.response.strip().startswith("{") and parsed.response.strip().endswith("}"):
        return False, "response字段不应包含JSON文本", None
    
    return True, "校验通过", parsed

raw = '{"intent": "create", "confidence": 0.95, "response": "已为您创建任务", "tool_calls": [{"name": "create_task"}]}'
is_valid, msg, result = validate_agent_output(raw)
print(f"校验结果: {is_valid}, 信息: {msg}")

代码解释: 这个校验函数实现了三步校验流程。第一步使用 json.loads() 检查输出是否为合法 JSON。第二步利用 Pydantic 的 ValidationError 自动完成类型检查和约束验证------Pydantic v2 在校验失败时返回详细的错误列表。第三步是业务规则校验,这是 Pydantic 无法自动完成的领域逻辑------例如"低置信度必须有解释"、"查询意图不应调用工具"等跨字段约束。

3.3 第二层:内容审查

内容审查关注输出内容是否包含敏感信息、是否违反业务规则。

python 复制代码
import re
from dataclasses import dataclass

@dataclass
class ContentCheckResult:
    """内容审查结果"""
    passed: bool
    violations: list[str]
    risk_level: str  # low, medium, high

class ContentValidator:
    """Agent 输出内容审查器"""
    
    def __init__(self):
        self.sensitive_patterns = {
            "phone": re.compile(r'1[3-9]\d{9}'),
            "id_card": re.compile(r'\d{17}[\dXx]'),
            "bank_card": re.compile(r'\d{16,19}'),
            "email": re.compile(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'),
        }
        self.forbidden_words = ["绝对保证", "100%有效", "包治百病", "无风险"]
        self.required_disclaimer = "本信息仅供参考"
    
    def check_sensitive_info(self, content: str) -> list[str]:
        """检查是否泄露敏感信息"""
        violations = []
        for info_type, pattern in self.sensitive_patterns.items():
            matches = pattern.findall(content)
            if matches:
                violations.append(f"检测到敏感信息[{info_type}]: {len(matches)}处")
        return violations
    
    def check_forbidden_words(self, content: str) -> list[str]:
        """检查禁止词汇"""
        violations = []
        for word in self.forbidden_words:
            if word in content:
                violations.append(f"包含禁止词汇: {word}")
        return violations
    
    def validate(self, content: str) -> ContentCheckResult:
        """执行完整内容审查"""
        all_violations = []
        all_violations.extend(self.check_sensitive_info(content))
        all_violations.extend(self.check_forbidden_words(content))
        if self.required_disclaimer not in content:
            all_violations.append("缺少必要的免责声明")
        
        if not all_violations:
            return ContentCheckResult(passed=True, violations=[], risk_level="low")
        
        sensitive_count = sum(1 for v in all_violations if "敏感信息" in v)
        if sensitive_count > 0:
            risk_level = "high"
        elif len(all_violations) > 2:
            risk_level = "medium"
        else:
            risk_level = "low"
        
        return ContentCheckResult(passed=False, violations=all_violations, risk_level=risk_level)

# 使用示例
validator = ContentValidator()
result = validator.validate("联系人电话13812345678,绝对保证准确。")
print(f"审查通过: {result.passed}, 风险等级: {result.risk_level}")
# 输出: 审查通过: False, 风险等级: high

代码解释: 内容审查器实现了三类检查。敏感信息检测使用正则表达式扫描输出中的手机号、身份证号、银行卡号等个人隐私信息------这些信息如果在 Agent 响应中泄露,可能违反数据保护法规。禁止词汇检查用于过滤虚假承诺类表述(如"绝对保证"),在金融、医疗等合规要求严格的场景中必须拦截。风险等级设定逻辑:敏感信息泄露为高风险(必须拦截),多项违规为中风险(需要人工审核),单项轻度违规为低风险。

3.4 第三层:事实核查与幻觉检测

事实核查是最复杂但也最重要的一层校验。它的目标是检测 Agent 输出中是否包含编造的内容。

python 复制代码
import json

class FactChecker:
    """基于多策略的事实核查器"""
    
    def __init__(self, llm_client):
        self.llm = llm_client
    
    def check_tool_result_consistency(self, agent_output: str, tool_results: list[dict]) -> dict:
        """检查 Agent 输出与工具实际返回结果是否一致"""
        consistency_prompt = f"""请检查以下Agent回复是否与工具实际返回结果一致。

## 工具实际返回结果
{json.dumps(tool_results, ensure_ascii=False, indent=2)}

## Agent回复内容
{agent_output}

## 检查要点
1. Agent回复中引用的数据是否来自工具返回结果?
2. Agent是否编造了工具未返回的数据?
3. Agent是否曲解了工具返回结果的含义?

输出JSON: {{"is_consistent": true/false, "violations": ["描述"], "fabricated_data": ["编造点"]}}
"""
        response = self.llm.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": consistency_prompt}],
            response_format={"type": "json_object"},
            temperature=0
        )
        return json.loads(response.choices[0].message.content)
    
    def check_self_consistency(self, question: str, num_samples: int = 3) -> dict:
        """自洽性检查:对同一问题多次采样,检查答案一致性"""
        answers = []
        for i in range(num_samples):
            response = self.llm.chat.completions.create(
                model="gpt-4o",
                messages=[
                    {"role": "system", "content": "请基于事实回答问题,不确定时请说明。"},
                    {"role": "user", "content": question}
                ],
                temperature=0.7,
            )
            answers.append(response.choices[0].message.content)
        
        check_prompt = f"""请判断以下{num_samples}个回答是否在事实层面一致。
如果核心事实相同但表述不同,视为一致。如果核心事实存在矛盾,视为不一致。

回答1: {answers[0]}
回答2: {answers[1]}
回答3: {answers[2]}

输出JSON: {{"consistent": true/false, "conflicts": ["矛盾点描述"]}}
"""
        response = self.llm.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": check_prompt}],
            response_format={"type": "json_object"},
            temperature=0
        )
        return json.loads(response.choices[0].message.content)
    
    def check_citation_accuracy(self, output: str, source_documents: list[str]) -> dict:
        """引用准确性检查:验证输出中的引用是否来自源文档"""
        citation_prompt = f"""请检查以下输出中的引用信息是否准确来自源文档。

## 源文档
{chr(10).join(f"文档{i+1}: {doc[:500]}..." for i, doc in enumerate(source_documents))}

## 待检查输出
{output}

输出JSON: {{"citations_accurate": true/false, "errors": ["错误描述"]}}
"""
        response = self.llm.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": citation_prompt}],
            response_format={"type": "json_object"},
            temperature=0
        )
        return json.loads(response.choices[0].message.content)

代码解释: 事实核查器实现了三种核心策略。check_tool_result_consistency 是最直接有效的幻觉检测方法------它将 Agent 的回复与工具实际返回的结果进行比对,检查 Agent 是否编造了工具未返回的数据或曲解了返回结果的含义。check_self_consistency 利用了一个重要特性:对于事实性问题,多次高温采样的结果应该一致;如果出现矛盾,说明模型可能在编造内容。check_citation_accuracy 适用于 RAG 场景,检查 Agent 输出的引用是否确实来自检索到的源文档。

3.5 校验机制的工程化考虑

在实际工程中,校验机制的设计还需要考虑以下因素:

考虑维度 说明 推荐方案
性能影响 每层校验都增加延迟 格式校验同步执行,语义校验异步执行
误报率 过于严格的校验会拒绝正确输出 设置灰度阈值,非二值判断
可扩展性 新增校验规则不应修改现有代码 采用策略模式或管道模式
可观测性 校验失败原因需要可追踪 记录详细日志和指标
降级策略 校验服务不可用时怎么办 降级为仅格式校验,标记需人工复核

四、自动重试策略:指数退避、上下文调整、模型切换

4.1 为什么要"自动重试"而不是"直接失败"?

在 Agent 系统中,输出质量问题有很大比例是非确定性 的------同样的输入,模型可能一次输出正确、一次输出错误。这种非确定性来源于模型采样的随机性。因此,当校验失败时,直接返回错误是一种浪费------更合理的策略是在控制成本的前提下进行有限次重试

但重试不是简单的"再跑一次"。盲目重试可能:

  • 得到完全相同的错误结果(模型记忆效应)
  • 浪费大量 token 和时间
  • 掩盖真正的系统性问题

因此,重试策略需要在每次重试时改变某些条件,以提高成功率。

4.2 重试策略的决策树

#mermaid-svg-MJ0jJ2SB1QgjoirF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MJ0jJ2SB1QgjoirF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MJ0jJ2SB1QgjoirF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MJ0jJ2SB1QgjoirF .error-icon{fill:#552222;}#mermaid-svg-MJ0jJ2SB1QgjoirF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MJ0jJ2SB1QgjoirF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MJ0jJ2SB1QgjoirF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MJ0jJ2SB1QgjoirF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MJ0jJ2SB1QgjoirF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MJ0jJ2SB1QgjoirF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MJ0jJ2SB1QgjoirF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MJ0jJ2SB1QgjoirF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MJ0jJ2SB1QgjoirF .marker.cross{stroke:#333333;}#mermaid-svg-MJ0jJ2SB1QgjoirF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MJ0jJ2SB1QgjoirF p{margin:0;}#mermaid-svg-MJ0jJ2SB1QgjoirF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MJ0jJ2SB1QgjoirF .cluster-label text{fill:#333;}#mermaid-svg-MJ0jJ2SB1QgjoirF .cluster-label span{color:#333;}#mermaid-svg-MJ0jJ2SB1QgjoirF .cluster-label span p{background-color:transparent;}#mermaid-svg-MJ0jJ2SB1QgjoirF .label text,#mermaid-svg-MJ0jJ2SB1QgjoirF span{fill:#333;color:#333;}#mermaid-svg-MJ0jJ2SB1QgjoirF .node rect,#mermaid-svg-MJ0jJ2SB1QgjoirF .node circle,#mermaid-svg-MJ0jJ2SB1QgjoirF .node ellipse,#mermaid-svg-MJ0jJ2SB1QgjoirF .node polygon,#mermaid-svg-MJ0jJ2SB1QgjoirF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MJ0jJ2SB1QgjoirF .rough-node .label text,#mermaid-svg-MJ0jJ2SB1QgjoirF .node .label text,#mermaid-svg-MJ0jJ2SB1QgjoirF .image-shape .label,#mermaid-svg-MJ0jJ2SB1QgjoirF .icon-shape .label{text-anchor:middle;}#mermaid-svg-MJ0jJ2SB1QgjoirF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MJ0jJ2SB1QgjoirF .rough-node .label,#mermaid-svg-MJ0jJ2SB1QgjoirF .node .label,#mermaid-svg-MJ0jJ2SB1QgjoirF .image-shape .label,#mermaid-svg-MJ0jJ2SB1QgjoirF .icon-shape .label{text-align:center;}#mermaid-svg-MJ0jJ2SB1QgjoirF .node.clickable{cursor:pointer;}#mermaid-svg-MJ0jJ2SB1QgjoirF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MJ0jJ2SB1QgjoirF .arrowheadPath{fill:#333333;}#mermaid-svg-MJ0jJ2SB1QgjoirF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MJ0jJ2SB1QgjoirF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MJ0jJ2SB1QgjoirF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MJ0jJ2SB1QgjoirF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MJ0jJ2SB1QgjoirF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MJ0jJ2SB1QgjoirF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MJ0jJ2SB1QgjoirF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MJ0jJ2SB1QgjoirF .cluster text{fill:#333;}#mermaid-svg-MJ0jJ2SB1QgjoirF .cluster span{color:#333;}#mermaid-svg-MJ0jJ2SB1QgjoirF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MJ0jJ2SB1QgjoirF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MJ0jJ2SB1QgjoirF rect.text{fill:none;stroke-width:0;}#mermaid-svg-MJ0jJ2SB1QgjoirF .icon-shape,#mermaid-svg-MJ0jJ2SB1QgjoirF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MJ0jJ2SB1QgjoirF .icon-shape p,#mermaid-svg-MJ0jJ2SB1QgjoirF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MJ0jJ2SB1QgjoirF .icon-shape .label rect,#mermaid-svg-MJ0jJ2SB1QgjoirF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MJ0jJ2SB1QgjoirF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MJ0jJ2SB1QgjoirF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MJ0jJ2SB1QgjoirF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 通过
失败
通过
失败
通过
失败


Agent 输出
格式校验
内容校验
格式重试
语义校验
内容重试
✅ 输出合格
语义重试
策略: 降低temperature

强化格式Prompt

启用JSON Mode
策略: 补充上下文

添加few-shot示例

调整系统提示词
策略: 切换更强模型

分解任务

引入外部知识
重试次数 < 上限?
重新生成
❌ 最终失败

人工介入

4.3 指数退避重试

指数退避是最基础的重试策略,核心思想是每次重试之间的等待时间按指数增长,避免在模型服务过载时雪上加霜。

python 复制代码
import time
import random
from typing import Callable, TypeVar, Optional
from dataclasses import dataclass
from enum import Enum

T = TypeVar('T')

class RetryReason(Enum):
    """重试原因分类"""
    FORMAT_ERROR = "format_error"
    CONTENT_VIOLATION = "content_violation"
    SEMANTIC_FAILURE = "semantic_failure"
    API_ERROR = "api_error"
    TIMEOUT = "timeout"

@dataclass
class RetryConfig:
    """重试配置"""
    max_retries: int = 3
    base_delay: float = 1.0
    max_delay: float = 30.0
    backoff_factor: float = 2.0
    jitter: bool = True
    
    def get_delay(self, attempt: int) -> float:
        """计算第 n 次重试的延迟时间"""
        delay = self.base_delay * (self.backoff_factor ** attempt)
        delay = min(delay, self.max_delay)
        if self.jitter:
            delay = delay * (0.5 + random.random() * 0.5)
        return delay

class RetryOrchestrator:
    """智能重试编排器"""
    
    def __init__(self, config: RetryConfig):
        self.config = config
        self.retry_history: list[dict] = []
    
    def execute_with_retry(
        self,
        generate_fn: Callable[..., T],
        validate_fn: Callable[[T], tuple[bool, str, RetryReason]],
        adjust_fn: Callable[[int, RetryReason, str], dict],
        initial_params: dict
    ) -> tuple[Optional[T], dict]:
        """带智能重试的执行器"""
        current_params = initial_params.copy()
        
        for attempt in range(self.config.max_retries + 1):
            try:
                output = generate_fn(**current_params)
                is_valid, error_msg, reason = validate_fn(output)
                
                if is_valid:
                    return output, {
                        "success": True,
                        "attempts": attempt + 1,
                        "history": self.retry_history
                    }
                
                self.retry_history.append({
                    "attempt": attempt + 1,
                    "reason": reason.value,
                    "error": error_msg,
                })
                
                if attempt < self.config.max_retries:
                    adjustments = adjust_fn(attempt, reason, error_msg)
                    current_params.update(adjustments)
                    delay = self.config.get_delay(attempt)
                    time.sleep(delay)
                    
                    print(f"重试 {attempt + 1}/{self.config.max_retries}: "
                          f"原因={reason.value}, 延迟={delay:.1f}s")
                
            except Exception as e:
                if attempt < self.config.max_retries:
                    self.retry_history.append({
                        "attempt": attempt + 1,
                        "reason": RetryReason.API_ERROR.value,
                        "error": str(e)
                    })
                    delay = self.config.get_delay(attempt)
                    time.sleep(delay)
                else:
                    raise
        
        return None, {
            "success": False,
            "attempts": self.config.max_retries + 1,
            "history": self.retry_history
        }

代码解释: 这是一个完整的智能重试编排器实现。RetryOrchestrator 的核心逻辑是"生成→校验→失败则调整参数→延迟→重试"。关键设计点包括:RetryConfig 使用指数退避算法计算延迟时间,加入随机抖动(jitter)避免多个客户端同时重试导致的"惊群效应";RetryReason 枚举区分不同失败原因,因为格式错误和语义错误需要不同的调整策略;retry_history 记录完整的重试链路,可用于后续分析和策略优化。

4.4 上下文调整策略

除了调整生成参数,另一个有效的重试策略是调整上下文------即在重试时修改 Prompt 中的信息,帮助模型纠正错误。

python 复制代码
from dataclasses import dataclass
from typing import Optional

class ContextAdjuster:
    """根据失败原因动态调整上下文"""
    
    def adjust_for_format_error(self, original_messages: list[dict],
                                  error_detail: str, attempt: int) -> list[dict]:
        """针对格式错误的上下文调整"""
        messages = [m.copy() for m in original_messages]
        format_reminder = f"""
## 重要提醒(第{attempt + 1}次尝试)
上一次输出存在格式错误:{error_detail}
请严格遵守:输出必须是合法的JSON,不要添加任何额外文字。
"""
        messages[0]["content"] += format_reminder
        
        # 第二次重试时添加正确输出的示例
        if attempt >= 1:
            messages.append({
                "role": "assistant",
                "content": '{"intent": "query", "confidence": 0.95, "response": "示例"}'
            })
            messages.append({"role": "user", "content": "请按照上述格式重新输出。"})
        
        return messages
    
    def adjust_for_hallucination(self, original_messages: list[dict],
                                   error_detail: str, attempt: int,
                                   tool_results: Optional[list[dict]] = None) -> list[dict]:
        """针对幻觉的上下文调整"""
        messages = [m.copy() for m in original_messages]
        
        if tool_results:
            facts = "## 工具实际返回结果(请严格基于以下事实回答)\n"
            facts += json.dumps(tool_results, ensure_ascii=False, indent=2)
            messages.append({"role": "system", "content": facts})
        
        messages[0]["content"] += f"""
## 反幻觉指令(第{attempt + 1}次尝试)
上一次输出检测到幻觉:{error_detail}
请严格遵守:只使用工具返回的数据,不要编造数据。
如果工具未返回某项信息,明确说明"信息不足"。
"""
        return messages

代码解释: ContextAdjuster 实现了针对不同失败原因的上下文调整策略。adjust_for_format_error 在重试时追加格式强调指令,并在第二次重试时注入正确格式示例作为 few-shot------这是非常有效的策略,因为模型可以从示例中学习期望的输出格式。adjust_for_hallucination 将工具实际返回结果作为 system 消息注入,为模型提供事实依据。这三种策略的共同思路是:不是简单地重试,而是在每次重试时为模型提供更多有用的信息

图:Agent 输出校验失败后的智能重试决策流程(指数退避、上下文调整、模型切换)

4.5 模型切换策略

当参数调整和上下文调整都无法解决问题时,切换到更强的模型是最后的手段。

切换策略 适用场景 成本变化 延迟变化
快→慢模型 简单格式问题 ↑ 30-50% ↑ 200-500ms
小→大模型 复杂推理问题 ↑ 200-300% ↑ 500-2000ms
同级不同厂商 厂商API故障 ±0% ±0%
模型→人工 全部模型失败 ↑↑↑ ↑↑↑
python 复制代码
class ModelFallbackChain:
    """模型降级链:从经济模型到强力模型到人工"""
    
    def __init__(self):
        self.chain = [
            {"model": "gpt-4o-mini", "role": "fast", "max_tokens": 2000},
            {"model": "gpt-4o", "role": "standard", "max_tokens": 4000},
            {"model": "gpt-4o", "role": "premium", "max_tokens": 8000,
             "extra_params": {"reasoning_effort": "high"}},
        ]
        self.current_index = 0
    
    def get_next_model(self, failure_reason: str) -> Optional[dict]:
        """获取下一个模型配置"""
        self.current_index += 1
        if self.current_index < len(self.chain):
            model_config = self.chain[self.current_index].copy()
            model_config["switch_reason"] = failure_reason
            return model_config
        return None  # 所有模型都已尝试,需要人工介入
    
    def reset(self):
        """重置到第一个模型"""
        self.current_index = 0

代码解释: ModelFallbackChain 实现了模型降级链机制。默认链路是 gpt-4o-mini(快速经济)→ gpt-4o(标准)→ gpt-4o with high reasoning(强力推理)。每次校验失败且重试次数耗尽后,调用 get_next_model 升级到下一个模型。这个设计的关键考虑是成本控制------日常请求使用经济模型即可,只有在质量不达标时才升级到更贵的模型。


五、质量评估:在线评估指标与离线评估方法

5.1 为什么需要质量评估?

校验机制和重试策略解决的是"单个请求"的质量问题,但无法回答更宏观的问题:Agent 系统的整体输出质量如何?某些类型的请求是否系统性地产出低质量结果?重试策略是否有效?要回答这些问题,需要建立完整的质量评估体系。

5.2 在线评估指标

在线评估指标是实时计算的,用于监控 Agent 系统的运行时质量状态。

python 复制代码
from collections import defaultdict
from dataclasses import dataclass
from typing import Optional
import threading

@dataclass
class QualityMetric:
    """质量指标数据结构"""
    total_requests: int = 0
    first_pass_success: int = 0
    final_success: int = 0
    format_errors: int = 0
    content_violations: int = 0
    semantic_failures: int = 0
    retry_count: int = 0
    avg_latency_ms: float = 0.0
    avg_retries: float = 0.0

class QualityMetricsCollector:
    """在线质量指标收集器(线程安全)"""
    
    def __init__(self):
        self._lock = threading.Lock()
        self._global_metrics = QualityMetric()
        self._intent_metrics: dict[str, QualityMetric] = defaultdict(QualityMetric)
    
    def record_request(self, intent: str, first_pass: bool,
                        final_success: bool, retry_count: int,
                        latency_ms: float, failure_reason: Optional[str] = None):
        """记录一次请求的质量数据"""
        with self._lock:
            g = self._global_metrics
            g.total_requests += 1
            if first_pass:
                g.first_pass_success += 1
            if final_success:
                g.final_success += 1
            g.retry_count += retry_count
            
            if failure_reason == "format_error":
                g.format_errors += 1
            elif failure_reason == "content_violation":
                g.content_violations += 1
            elif failure_reason == "semantic_failure":
                g.semantic_failures += 1
            
            n = g.total_requests
            g.avg_latency_ms = (g.avg_latency_ms * (n - 1) + latency_ms) / n
            
            intent_m = self._intent_metrics[intent]
            intent_m.total_requests += 1
            if first_pass:
                intent_m.first_pass_success += 1
            if final_success:
                intent_m.final_success_success += 1
            intent_m.retry_count += retry_count
    
    def get_dashboard_data(self) -> dict:
        """获取仪表盘数据"""
        with self._lock:
            g = self._global_metrics
            if g.total_requests > 0:
                g.avg_retries = g.retry_count / g.total_requests
            
            return {
                "summary": {
                    "total_requests": g.total_requests,
                    "first_pass_rate": f"{g.first_pass_success / g.total_requests * 100:.1f}%" if g.total_requests else "N/A",
                    "final_success_rate": f"{g.final_success / g.total_requests * 100:.1f}%" if g.total_requests else "N/A",
                    "avg_retries": f"{g.avg_retries:.2f}",
                    "avg_latency_ms": f"{g.avg_latency_ms:.0f}ms",
                },
                "failure_breakdown": {
                    "format_errors": g.format_errors,
                    "content_violations": g.content_violations,
                    "semantic_failures": g.semantic_failures,
                },
                "by_intent": {
                    intent: {
                        "total": m.total_requests,
                        "first_pass_rate": f"{m.first_pass_success / m.total_requests * 100:.1f}%" if m.total_requests else "N/A",
                    }
                    for intent, m in self._intent_metrics.items()
                }
            }

# 使用示例
collector = QualityMetricsCollector()
collector.record_request("query", True, True, 0, 350)
collector.record_request("create", False, True, 1, 1200, "format_error")
collector.record_request("update", False, False, 3, 3500, "semantic_failure")

import json
print(json.dumps(collector.get_dashboard_data(), ensure_ascii=False, indent=2))

代码解释: 在线指标收集器实现了线程安全的实时质量监控。核心指标包括:首次通过率(First Pass Rate)衡量模型直接输出质量------这个指标越高说明 Prompt 和 Schema 设计越好;最终成功率(Final Success Rate)衡量包含重试后的整体质量------这个指标反映系统级的可靠性;平均重试次数直接关联成本------重试越多 token 消耗越大。指标按意图分类(by_intent),可以快速定位哪类请求质量最差,从而针对性优化。

5.3 离线评估方法

离线评估用于在发布前系统性评估 Agent 输出质量,通常基于标注数据集进行。

python 复制代码
from dataclasses import dataclass
from typing import Optional
from collections import defaultdict

@dataclass
class EvalCase:
    """评估用例"""
    case_id: str
    user_input: str
    expected_intent: str
    expected_keywords: list[str]
    forbidden_words: list[str]
    tool_calls_expected: bool
    max_retries_allowed: int = 2

@dataclass
class EvalResult:
    """评估结果"""
    case_id: str
    passed: bool
    score: float
    issues: list[str]

class OfflineEvaluator:
    """离线评估器"""
    
    def __init__(self, test_cases: list[EvalCase]):
        self.test_cases = test_cases
        self.results: list[EvalResult] = []
    
    def evaluate_single(self, case: EvalCase, agent_output: str,
                        retries: int, latency_ms: float) -> EvalResult:
        """评估单个用例"""
        issues = []
        score = 100.0
        
        try:
            import json
            output = json.loads(agent_output)
        except Exception:
            return EvalResult(case.case_id, False, 0, ["JSON解析失败"])
        
        if output.get("intent") != case.expected_intent:
            issues.append(f"意图不匹配: 期望={case.expected_intent}, 实际={output.get('intent')}")
            score -= 25
        
        response_text = output.get("response", "")
        for kw in case.expected_keywords:
            if kw not in response_text:
                issues.append(f"缺少关键词: {kw}")
                score -= 10
        
        for word in case.forbidden_words:
            if word in response_text:
                issues.append(f"包含禁止词汇: {word}")
                score -= 15
        
        if retries > case.max_retries_allowed:
            issues.append(f"重试次数超限: {retries} > {case.max_retries_allowed}")
            score -= 20
        
        if case.tool_calls_expected and not output.get("tool_calls"):
            issues.append("期望包含工具调用但未包含")
            score -= 15
        
        return EvalResult(case.case_id, len(issues) == 0, max(0, score), issues)
    
    def generate_report(self) -> dict:
        """生成评估报告"""
        total = len(self.results)
        passed = sum(1 for r in self.results if r.passed)
        avg_score = sum(r.score for r in self.results) / total if total else 0
        
        issue_types = defaultdict(int)
        for r in self.results:
            for issue in r.issues:
                issue_types[issue.split(":")[0]] += 1
        
        return {
            "total_cases": total,
            "passed": passed,
            "pass_rate": f"{passed / total * 100:.1f}%" if total else "N/A",
            "avg_score": f"{avg_score:.1f}",
            "top_issues": sorted(issue_types.items(), key=lambda x: -x[1])[:5],
        }

代码解释: 离线评估器基于预定义的测试用例集进行系统评估。每个 EvalCase 定义了期望的意图、必须包含的关键词、禁止出现的词汇等约束。评估过程采用扣分制------初始 100 分,意图不匹配扣 25 分(最严重)、禁止词汇扣 15 分、关键词缺失扣 10 分、重试超限扣 20 分。generate_report 汇总所有评估结果,输出通过率、平均分和 Top 问题列表。这种量化评估方法可以用于版本迭代的回归测试、不同 Prompt 策略的 A/B 测试、以及不同模型的横向对比。


六、实战:一个完整的输出质量保障 Pipeline

6.1 Pipeline 架构设计

将前面讨论的所有组件整合为一个统一的 Pipeline,是工程落地的关键。

python 复制代码
"""Agent 输出质量保障 Pipeline - 整合格式控制、多层校验、智能重试、质量监控"""

import json
import time
import logging
from typing import Optional
from dataclasses import dataclass
from pydantic import BaseModel, Field, ValidationError
from openai import OpenAI

@dataclass
class PipelineConfig:
    """Pipeline 配置"""
    primary_model: str = "gpt-4o"
    fallback_model: str = "gpt-4o"
    temperature: float = 0.1
    max_retries: int = 3
    base_delay: float = 1.0
    backoff_factor: float = 2.0
    min_confidence: float = 0.5
    enable_content_review: bool = True
    enable_fact_check: bool = True
    enable_fallback_model: bool = True

class AgentOutput(BaseModel):
    """Agent 标准输出 Schema"""
    intent: str = Field(description="意图分类", pattern=r"^(query|create|update|delete)$")
    confidence: float = Field(description="置信度", ge=0.0, le=1.0)
    response: str = Field(description="Agent 回复", min_length=10, max_length=2000)
    tool_calls: list[dict] = Field(description="工具调用列表", default_factory=list)
    metadata: Optional[dict] = Field(description="元数据", default=None)

class QualityAssurancePipeline:
    """输出质量保障 Pipeline"""
    
    def __init__(self, config: PipelineConfig):
        self.config = config
        self.client = OpenAI()
        self.logger = logging.getLogger("QA.Pipeline")
    
    def run(self, user_input: str, system_prompt: str,
            tool_results: Optional[list[dict]] = None,
            intent_hint: str = "query") -> dict:
        """执行完整的质量保障 Pipeline"""
        start_time = time.time()
        messages = [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_input}
        ]
        
        if tool_results:
            messages.append({
                "role": "system",
                "content": f"工具返回结果:\n{json.dumps(tool_results, ensure_ascii=False)}"
            })
        
        # 带重试的生成与校验
        for attempt in range(self.config.max_retries + 1):
            try:
                # 生成
                response = self.client.beta.chat.completions.parse(
                    model=self.config.primary_model,
                    messages=messages,
                    response_format=AgentOutput,
                    temperature=max(0.0, self.config.temperature - attempt * 0.03)
                )
                output_text = response.choices[0].message.content
                parsed = AgentOutput(**json.loads(output_text))
                
                # 校验:内容审查
                if self.config.enable_content_review:
                    validator = ContentValidator()
                    content_result = validator.validate(parsed.response)
                    if not content_result.passed:
                        messages[0]["content"] += "\n注意:请确保输出不包含敏感信息和禁止词汇。"
                        continue
                
                # 校验:置信度
                if parsed.confidence < self.config.min_confidence and not parsed.metadata:
                    messages[0]["content"] += "\n注意:低置信度时请在metadata中说明原因。"
                    continue
                
                # 校验:事实核查
                if self.config.enable_fact_check and tool_results:
                    checker = FactChecker(self.client)
                    consistency = checker.check_tool_result_consistency(
                        parsed.response, tool_results
                    )
                    if not consistency.get("is_consistent", True):
                        messages[0]["content"] += (
                            "\n注意:请严格基于工具返回的数据回答,不要编造信息。"
                        )
                        continue
                
                # 全部通过
                latency_ms = (time.time() - start_time) * 1000
                return {
                    "success": True,
                    "output": parsed.model_dump(),
                    "metadata": {
                        "attempts": attempt + 1,
                        "latency_ms": round(latency_ms, 2),
                        "quality_flags": []
                    }
                }
                
            except (ValidationError, json.JSONDecodeError) as e:
                self.logger.warning(f"第{attempt+1}次尝试失败: {e}")
                messages[0]["content"] += (
                    f"\n重要:上次输出格式错误({str(e)[:100]}),请确保输出合法JSON。"
                )
                if attempt < self.config.max_retries:
                    delay = self.config.base_delay * (self.config.backoff_factor ** attempt)
                    time.sleep(delay)
            except Exception as e:
                self.logger.error(f"API错误: {e}")
                if attempt < self.config.max_retries:
                    time.sleep(self.config.base_delay * (self.config.backoff_factor ** attempt))
                else:
                    raise
        
        latency_ms = (time.time() - start_time) * 1000
        return {
            "success": False,
            "output": None,
            "metadata": {
                "attempts": self.config.max_retries + 1,
                "latency_ms": round(latency_ms, 2),
                "quality_flags": ["max_retries_exceeded"]
            }
        }

# 使用示例
if __name__ == "__main__":
    config = PipelineConfig(
        primary_model="gpt-4o",
        max_retries=3,
        enable_content_review=True,
        enable_fact_check=True
    )
    
    pipeline = QualityAssurancePipeline(config)
    
    tool_results = [{
        "tool": "query_user",
        "status": "success",
        "data": {"name": "张三", "department": "技术部", "join_date": "2023-06-15"}
    }]
    
    result = pipeline.run(
        user_input="查询张三的员工信息",
        system_prompt="你是人力资源助手。请查询并返回员工信息。",
        tool_results=tool_results,
        intent_hint="query"
    )
    
    print(json.dumps(result, ensure_ascii=False, indent=2))

代码解释: 这是完整的输出质量保障 Pipeline 实现,整合了前文讨论的所有组件。Pipeline 的执行流程是:构建请求消息 → 带重试的生成与校验 → 记录指标 → 返回结果。关键设计决策包括:使用 Structured Output 作为默认格式控制方案;校验分四层依次执行(格式→内容→置信度→事实核查),任一层失败即触发重试;参数调整策略根据失败原因动态变化------每次重试降低 temperature 并追加针对性指令。tool_results 作为事实依据注入到对话上下文中,并在事实核查阶段用于检测 Agent 是否编造了工具未返回的数据。最终结果包含完整的元数据(重试次数、延迟、质量标记),便于监控和调优。

6.2 Pipeline 的扩展性设计

#mermaid-svg-KB0WyycTnA3RMWmh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KB0WyycTnA3RMWmh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KB0WyycTnA3RMWmh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KB0WyycTnA3RMWmh .error-icon{fill:#552222;}#mermaid-svg-KB0WyycTnA3RMWmh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KB0WyycTnA3RMWmh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KB0WyycTnA3RMWmh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KB0WyycTnA3RMWmh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KB0WyycTnA3RMWmh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KB0WyycTnA3RMWmh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KB0WyycTnA3RMWmh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KB0WyycTnA3RMWmh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KB0WyycTnA3RMWmh .marker.cross{stroke:#333333;}#mermaid-svg-KB0WyycTnA3RMWmh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KB0WyycTnA3RMWmh p{margin:0;}#mermaid-svg-KB0WyycTnA3RMWmh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-KB0WyycTnA3RMWmh .cluster-label text{fill:#333;}#mermaid-svg-KB0WyycTnA3RMWmh .cluster-label span{color:#333;}#mermaid-svg-KB0WyycTnA3RMWmh .cluster-label span p{background-color:transparent;}#mermaid-svg-KB0WyycTnA3RMWmh .label text,#mermaid-svg-KB0WyycTnA3RMWmh span{fill:#333;color:#333;}#mermaid-svg-KB0WyycTnA3RMWmh .node rect,#mermaid-svg-KB0WyycTnA3RMWmh .node circle,#mermaid-svg-KB0WyycTnA3RMWmh .node ellipse,#mermaid-svg-KB0WyycTnA3RMWmh .node polygon,#mermaid-svg-KB0WyycTnA3RMWmh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KB0WyycTnA3RMWmh .rough-node .label text,#mermaid-svg-KB0WyycTnA3RMWmh .node .label text,#mermaid-svg-KB0WyycTnA3RMWmh .image-shape .label,#mermaid-svg-KB0WyycTnA3RMWmh .icon-shape .label{text-anchor:middle;}#mermaid-svg-KB0WyycTnA3RMWmh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KB0WyycTnA3RMWmh .rough-node .label,#mermaid-svg-KB0WyycTnA3RMWmh .node .label,#mermaid-svg-KB0WyycTnA3RMWmh .image-shape .label,#mermaid-svg-KB0WyycTnA3RMWmh .icon-shape .label{text-align:center;}#mermaid-svg-KB0WyycTnA3RMWmh .node.clickable{cursor:pointer;}#mermaid-svg-KB0WyycTnA3RMWmh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KB0WyycTnA3RMWmh .arrowheadPath{fill:#333333;}#mermaid-svg-KB0WyycTnA3RMWmh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KB0WyycTnA3RMWmh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KB0WyycTnA3RMWmh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KB0WyycTnA3RMWmh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KB0WyycTnA3RMWmh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KB0WyycTnA3RMWmh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KB0WyycTnA3RMWmh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KB0WyycTnA3RMWmh .cluster text{fill:#333;}#mermaid-svg-KB0WyycTnA3RMWmh .cluster span{color:#333;}#mermaid-svg-KB0WyycTnA3RMWmh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-KB0WyycTnA3RMWmh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KB0WyycTnA3RMWmh rect.text{fill:none;stroke-width:0;}#mermaid-svg-KB0WyycTnA3RMWmh .icon-shape,#mermaid-svg-KB0WyycTnA3RMWmh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KB0WyycTnA3RMWmh .icon-shape p,#mermaid-svg-KB0WyycTnA3RMWmh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KB0WyycTnA3RMWmh .icon-shape .label rect,#mermaid-svg-KB0WyycTnA3RMWmh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KB0WyycTnA3RMWmh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KB0WyycTnA3RMWmh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KB0WyycTnA3RMWmh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 可插拔组件
质量保障 Pipeline


请求入口
消息构建器
格式控制器
模型调用
格式校验器
内容审查器
事实核查器
校验通过?
输出封装
重试编排器
参数调整器
指标收集器
监控仪表盘
自定义校验规则
自定义审查策略
自定义重试策略
自定义评估指标

Pipeline 的每个组件都设计为可插拔的:

  • 格式校验器:可以替换或扩展 Schema,添加自定义校验规则
  • 内容审查器:可以配置不同的审查策略(敏感词列表、风险等级阈值)
  • 重试编排器:可以自定义重试策略(调整参数、切换模型、修改上下文)
  • 指标收集器:可以添加自定义指标(业务指标、SLA 指标)

6.3 生产环境部署建议

在生产环境部署质量保障 Pipeline 时,以下建议值得关注:

1. 异步校验与同步校验分层

格式校验(JSON 解析、Schema 验证)是同步执行的,因为它的耗时极短(<1ms)。但内容审查和事实核查涉及 LLM 调用,可能耗时数秒。对于延迟敏感的场景,可以采用"同步格式校验 + 异步语义校验"的策略:先通过格式校验快速返回结果,后台异步执行语义校验,如果不通过则触发告警。

2. 熔断与降级

当重试失败率持续升高时(如模型服务过载),Pipeline 应支持熔断------直接拒绝请求或降级为仅格式校验模式。熔断器的阈值可以基于滑动窗口的失败率来设定。

3. 成本监控

每次重试都消耗 token。Pipeline 应记录每个请求的 token 消耗和重试次数,并设置单请求的 token 上限。当单请求 token 消耗超过阈值时,停止重试并返回"质量保障失败"。

4. A/B 测试支持

在调整 Schema、修改 Prompt 或更换模型时,应该支持 A/B 测试------同时运行新旧两套配置,对比质量指标。这需要 Pipeline 支持多配置并行运行。


七、适用边界与风险提示

7.1 方案适用场景

本文的质量保障方案适用于以下场景:

  • 基于 API 调用的 Agent 系统:所有方案均基于 API 层面,不涉及模型训练
  • 中高频请求的生产环境:重试机制设计考虑了成本和延迟的平衡
  • 多种模型混用:校验机制与模型无关,可统一应用于不同厂商的模型

7.2 方案局限与风险

1. 事实核查的准确性依赖

本文的事实核查方案使用 LLM 进行一致性检查,这意味着"用 LLM 检查 LLM 的输出"。这种方法本身存在准确性问题------核查用的 LLM 也可能产生幻觉。在准确性要求极高的场景中,应考虑引入外部知识库或人工核查。

2. 重试的成本风险

每次重试都消耗额外的 token 和时间。如果 Agent 系统的初始质量很低(如首次通过率 <50%),重试机制可能带来显著的成本增加。在部署前应评估首次通过率,并根据成本预算调整最大重试次数。

3. 延迟风险

重试机制会引入不确定性延迟。对于实时性要求高的场景(如对话机器人),单次请求的最大延迟为 max_retries × (生成延迟 + 校验延迟 + 退避延迟),可能达到数十秒。建议设置总超时阈值。

4. 校验规则的维护成本

内容审查规则(敏感词列表、禁止词汇)需要持续维护和更新。随着业务场景的变化,新的合规要求不断出现。建议建立规则管理流程,定期审查和更新校验规则。

5. 模型厂商锁定风险

Structured Output 等高级能力依赖特定模型厂商的 API 支持。如果需要切换到不支持的模型,整个格式控制方案需要重构。建议在 Schema 定义层做抽象,使格式控制方案与具体模型解耦。


八、总结

Agent 输出质量保障不是单一的技术问题,而是一个贯穿"事前约束---事中校验---事后重试---持续评估"的系统性工程。本文从三大核心问题出发,构建了一套完整的质量保障框架:

事前约束层面,格式控制经历了从 Prompt 约束到 JSON Mode 再到 Structured Output 的演进。Structured Output 提供了 Schema 级别的格式保证,是目前最强的格式控制方案。但对于不支持该能力的模型,Prompt 约束加 Pydantic 后校验依然是可行的替代方案。关键原则是:在能力允许的范围内,尽早使用最强的格式控制方案。

事中校验层面,三层校验架构(格式校验→内容审查→事实核查)覆盖了从语法到语义的全维度质量检查。格式校验解决合法性问题,内容审查解决合规性问题,事实核查解决准确性问题。工程上需要注意性能影响和误报率,根据业务场景调整校验严格程度。

事后重试层面,智能重试策略的核心不是"盲目重试"而是"有策略地重试"------根据失败原因动态调整生成参数、修改上下文、切换模型。指数退避避免雪崩,上下文调整提高成功率,模型切换作为最后手段。关键是在成本和质量之间找到平衡点。

持续评估层面,在线指标监控运行时质量状态,离线评估用于版本迭代和回归测试。首次通过率、最终成功率、平均重试次数是三个核心北极星指标。

最后,质量保障不是"一次性建设"的工作,而是需要持续迭代的工程实践。随着业务场景的扩展和模型能力的演进,校验规则、重试策略、评估方法都需要不断调整和优化。建立"度量→分析→改进→验证"的闭环,才是质量保障的长期之道。

图:Agent 输出质量保障体系从在线监控到离线评估再到持续改进的完整闭环


参考资料

  1. OpenAI. "Structured Outputs." OpenAI API Documentation, 2024. https://platform.openai.com/docs/guides/structured-outputs
  2. Pydantic Team. "Pydantic v2 Documentation." 2024. https://docs.pydantic.dev/latest/
  3. OpenAI. "JSON Mode." OpenAI API Reference, 2023. https://platform.openai.com/docs/guides/text-generation/json-mode
  4. Anthropic. "Tool Use with Claude." Anthropic API Documentation, 2024. https://docs.anthropic.com/en/docs/build-with-claude/tool-use
  5. Manakul, P., Liusie, A., & Gales, M.J.F. (2023). "SelfCheckGPT: Zero-Resource Black-Box Hallucination Detection for Generative Large Language Models." arXiv:2303.08896
  6. Lewis, P. et al. (2020). "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks." NeurIPS 2020
  7. 宽海辽. "AI Agent 工程化落地实战系列." CSDN博客, 2024.
  8. Hunter, J. "Exponential Backoff And Jitter." AWS Architecture Blog, 2015. https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
相关推荐
烛之武1 小时前
LangChain笔记
langchain·大模型·agent·mcp
gs801401 小时前
人工智能前沿技术动态与系统级演进图谱 20260918
ai
爱上纯净的蓝天1 小时前
自包含推理一体机四层架构拆解:128G 统一内存、256K 上下文与实测口径
人工智能·大模型·私有化部署·agent·大模型部署
一心同学1 小时前
Hermes架构拆解之Agent Loop
人工智能·agent·loop·hermes
七夜zippoe1 小时前
Agent 上下文工程:Token 管理、上下文压缩与分层记忆设计
ai·agent·token·上下文压缩·分层记忆
HRaitest1 小时前
【架构拆解】从“外挂插件”到“原生基座”:2026 新一代全链路 AI 招聘系统底层技术演进
人工智能·ai·求职招聘
一个金牛座的前端1 小时前
AI 写前端,优化的是演示,不是交付
前端·ai·cursor
OxYGC1 小时前
[AI工程] Spring AI第一篇:2.0 到底升级了什么?从 Prompt、RAG、MCP 到 Agent 应用实战
ai·ai编程·ai-native
晓窗科技2 小时前
AI基座优秀服务商
ai