Skip to content

输出质量

8. 结构化输出(Structured Output)

难度:中级 | 模式:循环 + 重试 + 校验 | 关键词:JSON 解析、格式验证、可靠输出

8.1 架构

通过"生成 → 校验 → 提取"三步循环,确保 LLM 输出符合预期的 JSON 格式:

结构化输出:生成 → 解析校验 → 提取输出,校验失败则重试

8.2 核心思路

LLM 的输出是自由文本,但下游系统往往需要结构化数据(JSON、表格、特定格式)。核心挑战是:LLM 可能输出格式不对的内容。解决方案:生成 → 解析校验 → 不对就重来

8.3 关键代码

实现分为三步:GenerateJSON 生成、ValidateJSON 解析校验(带 max_retries)、CheckResult 提取输出或决策重试。注意双层重试机制 —— 节点内重试解析,Flow 层重试生成:

python
import json
import re
from pocketflow import Node, Flow

class GenerateJSON(Node):
    def prep(self, shared):
        return shared["task"]

    def exec(self, task):
        prompt = f"""请为以下任务生成严格的 JSON 格式结果:
{task}

输出格式:{{"name": "...", "score": 0-100, "reason": "..."}}
只输出 JSON,不要其他文字。"""
        return call_llm(prompt)

    def post(self, shared, prep_res, exec_res):
        shared["raw_output"] = exec_res

class ValidateJSON(Node):
    """解析并校验 JSON 格式,利用 max_retries 自动重试解析"""
    def prep(self, shared):
        return shared["raw_output"]

    def exec(self, raw):
        # 提取 JSON 部分
        match = re.search(r'\{.*\}', raw, re.DOTALL)
        if not match:
            raise ValueError("输出中未找到 JSON")
        data = json.loads(match.group())
        # 校验必需字段和类型
        assert "name" in data, "缺少 name 字段"
        assert "score" in data, "缺少 score 字段"
        assert isinstance(data["score"], (int, float)), "score 必须是数字"
        assert 0 <= data["score"] <= 100, "score 必须在 0-100 之间"
        return data

    def exec_fallback(self, prep_res, exc):
        # 解析失败,返回 None 触发重新生成
        print(f"解析失败:{exc}")
        return None

    def post(self, shared, prep_res, exec_res):
        shared["result"] = exec_res  # 写入解析结果(成功为 dict,失败为 None)

class CheckResult(Node):
    def prep(self, shared):
        return shared.get("result")

    def post(self, shared, prep_res, exec_res):
        if shared.get("result") is None:
            shared["retry_count"] = shared.get("retry_count", 0) + 1
            if shared["retry_count"] >= 3:
                return "give_up"
            return "retry"       # 解析失败,让 LLM 重新生成
        return "done"

# 构建 Flow
generate = GenerateJSON()
validate = ValidateJSON(max_retries=2)  # 解析本身可重试 2 次
check = CheckResult()
output = Node()  # 占位输出节点

generate >> validate >> check
check - "retry" >> generate      # 格式不对,重新生成
check - "done" >> output         # 格式正确,输出结果
check - "give_up" >> output      # 多次失败,放弃

flow = Flow(start=generate)
flow.run({"task": "评估候选人张三的 Python 编程能力"})

学习要点

  • 双层重试ValidateJSON(max_retries=2) 在节点内重试解析,check - "retry" >> generate 在 Flow 层重试生成
  • exec_fallback:解析失败时不抛异常,而是返回 None 让后续节点决策
  • 防御性解析:用正则提取 JSON、逐字段校验,应对 LLM 输出的不确定性
  • 退出条件:设置最大重试次数,避免无限循环

9. 思维链推理(Chain-of-Thought)

难度:进阶 | 模式:循环 + 自检 | 关键词:分步推理、自我验证、复杂问题求解

9.1 架构

思维链:循环推理 + 思考历史存储

分步推理 + 自我验证的循环架构 —— 每推一步就校验,错了则回退重推。Verify 有三条出路:步骤有误时 "error" 回退重推;步骤正确但未完成时 "continue" 继续下一步;全部完成时 "ok" 进入 Conclude 输出答案。

9.2 核心思路

复杂问题(数学、逻辑、多步规划)直接让 LLM 一步回答容易出错。解决方案:分步推理,每步验证

  1. StepReason:每次只推理一步,追加到推理链
  2. Verify:检查最新一步是否正确,不正确则回退重推
  3. Conclude:推理完成后,整合所有步骤给出最终答案

9.3 关键代码

三个节点各司其职:StepReason 推理一步、Verify 校验正确性、Conclude 整合答案。shared["steps"] 列表记录完整推理链,验证失败时 pop() 回退:

python
from pocketflow import Node, Flow

class StepReason(Node):
    def prep(self, shared):
        return {
            "question": shared["question"],
            "steps": shared.get("steps", []),
        }

    def exec(self, data):
        prompt = f"""问题:{data['question']}
已有推理步骤:{data['steps']}
请继续推理下一步,输出格式:
STEP: [推理过程]
ANSWER: [如果已得出最终答案]"""
        return call_llm(prompt)

    def post(self, shared, prep_res, exec_res):
        shared.setdefault("steps", []).append(exec_res)
        shared["latest_step"] = exec_res

class Verify(Node):
    def prep(self, shared):
        return {
            "steps": shared["steps"],
            "latest_step": shared["latest_step"],
        }

    def exec(self, data):
        prompt = f"请验证以下推理是否正确:\n{data['steps']}\n如果有错误请指出。"
        return call_llm(prompt)

    def post(self, shared, prep_res, exec_res):
        if "错误" in exec_res:
            shared["steps"].pop()       # 移除错误的步骤
            return "error"              # 回退重推
        if "ANSWER" in shared.get("latest_step", ""):
            return "ok"                 # 已得出答案
        return "continue"               # 正确但未完成

class Conclude(Node):
    def prep(self, shared):
        return shared["steps"]

    def exec(self, steps):
        prompt = f"基于以下推理步骤,给出最终答案:\n{steps}"
        return call_llm(prompt)

    def post(self, shared, prep_res, exec_res):
        shared["answer"] = exec_res

# 构建 Flow
step_reason = StepReason()
verify = Verify()
conclude = Conclude()

step_reason >> verify
verify - "error" >> step_reason     # 发现错误,重推
verify - "continue" >> step_reason  # 继续推理下一步
verify - "ok" >> conclude           # 验证通过,输出

flow = Flow(start=step_reason)
flow.run({"question": "一个水池有 A、B 两个进水管,A 管 4 小时注满,B 管 6 小时注满,同时开两管几小时注满?"})

学习要点

  • 分步推理:每次只推理一步,降低单步出错概率
  • 自我验证:Verify 节点检查推理正确性,错误则回退
  • 步骤管理shared["steps"] 列表记录完整推理链,验证失败时 pop() 回退
  • 与结构化输出的区别:结构化输出校验格式,思维链校验逻辑

Easy-Pocket —— 从零掌握 PocketFlow