摘要
在 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 系统中最复杂的质量问题,因为它跨越了"文本生成"和"工具执行"两个领域。
常见的工具调用错误包括:
- 参数缺失:Agent 在调用工具时省略了必填参数
- 参数类型错误:期望传入 integer,实际传入 string
- 调用顺序错误:先调用需要认证的 API,但还没调用登录接口
- 工具幻觉:调用了一个不存在的工具或调用了错误的工具版本
- 结果误读:工具返回了错误码,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": "用户原始输入文本"
}
注意事项
- 只输出JSON,不要输出任何其他内容
- 不要在JSON前后添加markdown标记或解释文字
- 如果无法提取某个字段,使用null值
- 数组字段至少返回一个元素,可为空数组
"""
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 输出质量保障体系从在线监控到离线评估再到持续改进的完整闭环
参考资料
- OpenAI. "Structured Outputs." OpenAI API Documentation, 2024. https://platform.openai.com/docs/guides/structured-outputs
- Pydantic Team. "Pydantic v2 Documentation." 2024. https://docs.pydantic.dev/latest/
- OpenAI. "JSON Mode." OpenAI API Reference, 2023. https://platform.openai.com/docs/guides/text-generation/json-mode
- Anthropic. "Tool Use with Claude." Anthropic API Documentation, 2024. https://docs.anthropic.com/en/docs/build-with-claude/tool-use
- Manakul, P., Liusie, A., & Gales, M.J.F. (2023). "SelfCheckGPT: Zero-Resource Black-Box Hallucination Detection for Generative Large Language Models." arXiv:2303.08896
- Lewis, P. et al. (2020). "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks." NeurIPS 2020
- 宽海辽. "AI Agent 工程化落地实战系列." CSDN博客, 2024.
- Hunter, J. "Exponential Backoff And Jitter." AWS Architecture Blog, 2015. https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/