Chapter5 生成:格式化生成

本文学习来源 all-in-rag
个人学习笔记整理总结,有错误或者遗漏希望大家指正

预览

LLM 的输出不能只适合人阅读,还要适合程序解析、校验、渲染和执行。
格式化生成负责把“模型会说话”变成“系统能处理”。

格式化生成的核心价值是把自然语言能力接到工程系统里:

用户输入
-> LLM 理解意图
-> 输出 JSON / Pydantic 对象 / tool_calls
-> 代码解析、校验、调用工具或渲染页面
-> 系统继续执行确定性的业务逻辑

1. 为什么需要格式化生成

普通 LLM 输出通常是自然语言文本,适合阅读,但不适合直接被代码稳定处理。

例如电商客服里,用户问:

推荐几款适合程序员的键盘

如果模型只返回一段介绍文字,前端很难直接渲染商品卡片。更理想的输出是:

[
  {
    "name": "键盘名称",
    "price": 399,
    "features": ["机械轴", "蓝牙", "多设备切换"],
    "url": "https://example.com"
  }
]

格式化生成适合这些场景:

  • RAG 结果结构化展示:把答案拆成标题、摘要、引用来源、相关条目。
  • 自然语言转 API 参数:把“查明天上海到北京的航班”解析成出发地、目的地和日期。
  • 信息抽取:从文章、合同、简历、客服记录中抽取实体、时间、地点、事件。
  • Agent 工具调用:让模型判断该调用哪个工具,并生成工具参数。

关键点:

自然语言输出解决“表达”问题。
结构化输出解决“程序继续处理”问题。

2. LangChain Output Parsers

LangChain 的 OutputParsers 负责两件事:

  1. 在 Prompt 中注入格式要求。
  2. 在模型返回后,把文本解析成目标结构。

常见解析器:

  • StrOutputParser:把模型输出当字符串返回。
  • JsonOutputParser:把 JSON 字符串解析成 Python 字典或列表。
  • PydanticOutputParser:用 Pydantic 模型定义结构,并做类型校验。

2.1 PydanticOutputParser 的核心流程

定义 Pydantic Schema
-> 创建 PydanticOutputParser
-> 生成 format_instructions
-> 注入 PromptTemplate
-> prompt | llm | parser
-> 得到 Pydantic 对象

例如代码示例

class PersonInfo(BaseModel):
    name: str = Field(description="人物姓名")
    age: int = Field(description="人物年龄")
    skills: List[str] = Field(description="技能列表")

这里的 Field(description=...) 不只是给人看的注释,它会进入格式指令,影响模型怎么理解字段。

2.2 format_instructions 做了什么

parser.get_format_instructions() 会把 Pydantic 模型转换成类似 JSON Schema 的说明,然后塞进 Prompt。

可以理解成:

开发者定义 Python 类型
-> parser 转成模型可理解的格式说明
-> LLM 按格式说明输出 JSON
-> parser 再把 JSON 校验成 Python 对象

这比单纯写一句“请输出 JSON”更稳,因为 Schema 更明确,而且返回后还有校验。

2.3 解析与校验

PydanticOutputParser 收到模型输出后会做两层处理:

  1. 先把文本解析成 JSON。
  2. 再用 Pydantic 模型做字段和类型校验。

如果输出是:

{
  "name": "张三",
  "age": 30,
  "skills": ["Python", "Go语言"]
}

最终会得到:

PersonInfo(name='张三', age=30, skills=['Python', 'Go语言'])

注意:

Output Parser 不能保证模型一定生成正确格式。
它的价值是增强约束,并在格式错误时尽早暴露问题。

3. LlamaIndex 的结构化输出

LlamaIndex 在 RAG 流程里更强调响应合成和结构化输出。

3.1 Response Synthesizer

在普通 RAG 中,检索器召回 Nodes 后,Response Synthesizer 会负责组织上下文并生成答案。

常见模式:

  • refine:逐块处理检索内容,迭代改进答案。
  • compact:尽量把更多上下文压缩进单次 LLM 调用。

默认情况下,它生成的是自然语言文本。

3.2 Pydantic Programs

当需要结构化数据时,LlamaIndex 主要使用 Pydantic Programs。

核心流程:

定义 Pydantic 模型
-> LlamaIndex 转成格式约束
-> LLM 生成结构化结果
-> Pydantic 校验
-> 返回 Pydantic 对象

如果底层模型支持 Function Calling,LlamaIndex 会优先使用工具调用能力;如果不支持,则退回到把 JSON Schema 注入 Prompt 的方式。

这和 LangChain 的思想接近:

Schema 是开发者和模型之间的结构化契约。

4. 不依赖框架的实现思路

不用 LangChain 或 LlamaIndex,也可以通过提示词工程控制输出格式。

常用方法:

  • 明确要求 JSON:要求“只返回 JSON,不要解释,不要 Markdown”。
  • 提供 JSON Schema:写清楚字段名、类型、含义、是否必填。
  • 提供 few-shot 示例:给出输入和标准输出,让模型模仿。
  • 使用语法约束:本地模型可通过 GBNF 等 grammar 强制输出合法语法。

一个最小提示模板:

请从文本中抽取人物信息。
只返回 JSON,不要输出解释。

JSON 字段:
- name: string,人物姓名
- age: number,人物年龄
- skills: string[],技能列表

文本:
张三今年30岁,他擅长Python和Go语言。

这种方式实现简单,但缺点也明显:

  • 约束主要靠 Prompt,稳定性依赖模型遵循指令的能力。
  • 需要自己写解析、校验、重试和错误处理。
  • 字段变多后,提示词容易变长,也容易遗漏边界情况。

5. Function Calling

Function Calling 也叫 Tool Calling。它不是让模型真的执行函数,而是让模型生成一个结构化的“调用请求”。

核心分工:

模型负责判断:该不该调用工具,调用哪个工具,参数是什么。
代码负责执行:解析 tool_calls,调用真实 API,把结果返回给模型。

5.1 工作流程

完整链路是:

1. 代码定义 tools
2. 用户提问
3. 模型返回 tool_calls
4. 代码解析工具名和参数
5. 代码实际执行工具
6. 代码把工具结果作为 role=tool 的消息发回模型
7. 模型基于工具结果生成最终回答

代码示例中的工具定义是天气查询:

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定地点的天气信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "城市和省份,例如:杭州市, 浙江省",
                    }
                },
                "required": ["location"]
            },
        }
    },
]

用户问:

杭州今天天气怎么样?

模型不会直接查天气,而是可能返回:

tool_name: get_weather
arguments: {"location": "杭州市, 浙江省"}

然后代码模拟执行工具:

24℃,晴朗

最后把这个工具结果发回模型,由模型组织成自然语言回答。

5.2 Function Calling 的关键理解

最重要的一点:

LLM 不执行工具,应用程序执行工具。

模型输出的是意图和参数。真正的网络请求、数据库查询、文件操作、支付、发消息,都必须由代码完成。

所以 Function Calling 的可靠性来自两部分:

  • 模型原生支持结构化工具调用,比纯文本 JSON 更稳定。
  • 代码层可以对工具名、参数、权限、返回结果做严格控制。

5.3 Function Calling 的优势

相比“让模型直接输出 JSON”,Function Calling 更适合需要执行动作的场景。

优势:

  • 结构更稳定:模型返回的是标准化的 tool_calls
  • 意图识别更强:模型可以从多个工具中选择合适工具。
  • 可以接外部系统:API、数据库、搜索、订单系统、文件系统都可以变成工具。
  • 适合构建 Agent:Agent 的核心就是规划、调用工具、观察结果、继续推理。

但也要注意:

Function Calling 不是安全边界。
安全边界必须由代码侧实现。

例如:

  • 工具参数要校验。
  • 工具调用要有权限控制。
  • 高风险动作要二次确认。
  • 外部 API 错误要可恢复。
  • 工具返回不能盲目信任,需要清洗或标注来源。

6. 总结

方案 适合场景 优点 局限
Prompt 要求 JSON 简单结构化输出 实现成本最低 稳定性一般,需要自己解析校验
JSON Schema + few-shot 字段较明确的信息抽取 比纯指令更清晰 仍依赖模型遵循提示
LangChain Output Parser Python 应用内结构化抽取 能生成格式指令并自动解析 格式错误时仍需重试策略
Pydantic 模型 强类型结构化结果 字段、类型、描述集中管理 Schema 设计质量很关键
LlamaIndex Pydantic Programs RAG 响应需要结构化 与检索和响应合成结合好 依赖 LlamaIndex 工作流
Function Calling 需要调用工具或 API 原生工具调用,适合 Agent 工具执行、安全和错误处理都在代码侧
Grammar 约束 本地模型强约束输出 token 级别限制格式 配置成本更高,灵活性较低

选择建议:

只需要抽取结构化字段:优先 PydanticOutputParser / Pydantic Programs。
需要调用外部工具:优先 Function Calling。
只做轻量 demo:Prompt + JSON Schema 可以够用。
本地模型且必须严格合法 JSON:考虑 grammar 约束。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐