10 结构化输出与评估

QuibblerAgentQuibblerAgent 2026-11-13 约 19 分钟 15 次阅读

模型回你一段"课程已完成前五课",程序没法用——数据库要的是结构化字段;你改了一版提示词,效果变好了吗——"感觉"不算数。本文是「从零手写 Agent」系列第 10 篇,两个工程化硬需求一次讲完:结构化输出让模型的结果可解析,评估让改进可衡量。上一篇的 RAG 让 Agent 查得到资料,这一篇保证它交得出机器能用的结果、且每一次改进都有数字背书。

本文按"结构化输出套路 → 迷你评估框架 → 评估方法论 → 总结"组织,全部数字来自实测。

1、结构化输出:工具提交法

让模型输出受控 JSON,最稳的手写方案借用了系列第 4 篇的机制——最终答案必须通过调用工具提交。给模型唯一一个工具,字段类型由 JSON Schema 声明:

SUBMIT_TOOL = {
    "name": "submit_result",
    "description": "提交最终结构化结果。完成分析后必须调用本工具提交。",
    "input_schema": {
        "type": "object",
        "properties": {
            "course_name":  {"type": "string", "description": "课程名"},
            "phases":       {"type": "number", "description": "阶段数量"},
            "done_lessons": {"type": "number", "description": "已完成课数"},
            "next_lesson":  {"type": "string", "description": "下一课名"},
        },
        "required": ["course_name", "phases", "done_lessons", "next_lesson"],
    },
}

解读:只给这一个工具、系统提示词强调"必须通过工具提交",模型的最终输出就成了带 schema 约束的调用块,从响应里取出 input 即得字段齐全、类型正确的字典。实测用它从课程 README 提取进度,四个字段类型全对。实测返回的结构化结果如下:

{
  "course_name": "AgentDev · 从零手写 Agent 学习项目",
  "phases": 4,
  "done_lessons": 5,
  "next_lesson": "L6"
}

解读:phases 与 done_lessons 是真数字而非字符串,必填字段一个不少——这样的结果可以直接 json.dumps 进数据库,接进任何下游程序。这套"以工具调用为输出契约"的思路,正是官方 structured outputs 特性的原理前身:把输出格式从"祈祷模型守规矩"变成"协议强制校验"。

顺带交代为什么不用最"省事"的做法——提示词里贴一份 JSON 模板让模型照抄。那条土路没有任何校验:模型时而给 JSON 套上 markdown 代码围栏,时而擅自加一句寒暄,时而在数字外面补引号;下游解析器每堵一种花式就冒出下一种。工具提交法把格式从社交请求变成接口契约,才算工程方案。

1.1、解析与校验

工具提交法落地要补两个函数:从响应里捞出调用块,以及不信任任何输入的本地校验。实现如下:

def extract_structured(resp) -> dict | None:
    for block in resp.content:
        if block.type == "tool_use" and block.name == "submit_result":
            return block.input                 # SDK 已保证是 dict
    return None

def validate(data: dict, schema: dict) -> list[str]:
    errors = []
    for field in schema["required"]:           # 必填逐项核对
        if field not in data:
            errors.append(f"缺字段 {field}")
    for k, spec in schema["properties"].items():
        if spec["type"] == "number" and isinstance(data.get(k), str):
            errors.append(f"{k} 应为数字,得到字符串")
    return errors

解读:哪怕 schema 声明了类型,接口侧也不保证百分百执行,校验函数是最后一道闸。实测两百次提交里类型填错零次,真正的失败模式是模型绕开工具直接文字作答,出现三次——所以校验之外还得有兜底的纠偏循环,把"没调工具"与"调了但字段不合法"都变成喂回模型的错误数据:

def ask_structured(task: str) -> dict:
    history = [{"role": "user", "content": task}]
    for _ in range(2):                          # 最多一次纠偏重试
        resp = client.messages.create(
            model=MODEL, max_tokens=1024, tools=[SUBMIT_TOOL],
            system="完成后必须调用 submit_result 提交。", messages=history)
        history.append({"role": "assistant",    # 点菜单原样入历史
                        "content": [b.model_dump() for b in resp.content]})
        call = next((b for b in resp.content if b.type == "tool_use"), None)
        if call is None:                        # 没调工具:文字催一次
            history.append({"role": "user",
                            "content": "请调用 submit_result 提交。"})
            continue
        errs = validate(call.input, SUBMIT_TOOL["input_schema"])
        if not errs:
            return call.input                   # 校验通过即收货
        history.append({"role": "user", "content": [{   # 报错回传
            "type": "tool_result", "tool_use_id": call.id,
            "is_error": True, "content": ";".join(errs)}]})
    raise RuntimeError("两轮未产出合法结果")

解读:这套"提交、校验、报错、重试"与第 4 篇"错误也是数据"一脉相承——校验失败不是异常,是纠偏素材。实测三次绕开工具的运行全部被第一轮催回;两轮上限是防打转的安全阀,与第 5 篇的 max_turns 同一个思想。

1.2、官方语法约束特性

官方后来把这件事做成了正式特性:beta 阶段叫 output_format 参数(要带 beta 请求头),截至本文写作已转正为 output_config.format,把 JSON Schema 编译成语法约束,解码时直接禁止不合语法的 token——不是"答完再查",是"答的时候就只能这么答"。原理上仍是工具提交法的那套契约,只是执行点从你的校验代码前移到了模型的解码器。两条路线对比如下:

维度 工具提交法 官方结构化输出
约束时机 生成后校验,失败重试 解码时语法强制
适用端点 任何支持工具调用的接口 需新版接口参数
额外成本 可能多一次往返 schema 编译开销
表达力 与工具参数同构 递归 schema 不支持

解读:手写阶段建议两种都留。工具提交法不挑端点,且与 Agent 循环天然咬合——工具调用本来就是循环的一等公民;官方特性适合一次性抽取场景,一条请求出去就是保证合法的 JSON。两者也能组合:官方特性保证形状合法,本地校验再补业务规则——数字范围、枚举取值这类 schema 表达不了的约束。细节以官方 structured outputs 文档为准。

2、迷你评估框架

评估三板斧:测试集、跑分、对比。一个不到四十行的评估循环如下:

TEST_CASES = [
    ("计算 1234 + 567", 1801),
    ("999 除以 3 是多少", 333),
    ("计算 0.1 + 0.2", 0.3),       # 浮点精度陷阱
]

def run_eval() -> int:
    correct = 0
    for question, expected in TEST_CASES:
        agent = Agent(tools_spec=CALC_SPEC, tool_impls={"calculate": calc},
                      system="所有计算必须用工具,最终答案只输出数字。",
                      max_turns=4, verbose=False)
        got = extract_number(agent.run(question))    # 宽松抽取首个数字
        correct += abs(got - expected) < 1e-6        # 数值容差判分
    return correct

解读:每个用例起一个干净 Agent 避免相互污染;判分函数按场景选宽松度,分类题用精确匹配、计算题用数值容差、开放题可上模型当裁判。八道算术题的实测结果是八比八,连 0.1 加 0.2 的浮点陷阱也被容差覆盖——评估器本身的精度设计,是被很多团队忽略的必修课。

2.1、判分函数三档宽松度

判分器是评估的地基。上面那行 extract_number 与三档判分函数的完整实现如下:

import re

def extract_number(text: str) -> float:
    m = re.search(r"-?\d+(?:\.\d+)?", text)     # 宽松抽取首个数字
    if not m:
        raise ValueError("回答里找不到数字")
    return float(m.group())

def judge_exact(got, expected):                 # 一档:精确匹配
    return str(got).strip() == str(expected)

def judge_number(got, expected, tol=1e-6):      # 二档:数值容差
    return abs(float(got) - float(expected)) < tol

def judge_contains(got, expected):              # 三档:关键词在场
    return str(expected) in str(got)

解读:三档从紧到松,选错档会得出相反的结论——太紧把对的判错(0.3 对 0.30000000004),太松把错的判对(抽取器捞到题干里的数字)。判分器写错是最隐蔽的 bug:它不报错,只是安静地把每次改进判成无效、或把每次退步判成进步。上线前先拿三组已知对错的标准答案,把判分器本身测一遍。

测试集从哪来也别凭空造。第一版从自己的真实翻车记录里攒:每次 Agent 答错而你知道对错的案例,原样收进用例库;跑通一轮后再补边界题与陷阱题。八道题里三道来自真实翻车,比三十道拍脑袋编的题有用——题目立场要站在系统的对立面,专挑它不会的地方问。

2.2、对比闭环的实现

评估的价值在对比:同一测试集、两个配置各跑一遍看差值。把开头的 run_eval 参数化成 run_suite,基线对比脚本如下:

def run_suite(system: str) -> int:
    correct = 0
    for question, expected in TEST_CASES:
        agent = Agent(tools_spec=CALC_SPEC,
                      tool_impls={"calculate": calc},
                      system=system, max_turns=4, verbose=False)
        try:
            got = extract_number(agent.run(question))
        except ValueError:
            continue                             # 抽不到数字记零分
        correct += judge_number(got, expected)
    return correct

base = run_suite("所有计算必须用工具,最终答案只输出数字。")
edit = run_suite("所有计算必须用工具。")           # 删掉约束后半句
print(f"基线 {base}/{len(TEST_CASES)} → 改动后 {edit}/{len(TEST_CASES)}")

解读:脚本的输出就是结论本身,实测记录如下:

基线 8/8 → 改动后 6/8
失败两题:999 除以 3、0.1 加 0.2
失败原因:回答混入解释文字,抽取器捞到题干里的 999 与 0.1

解读:失败机理值得细品——不是模型算错了,是它开始说人话,而抽取器只认第一个数字。改提示词伤到的是提示词与判分器之间的隐性契约,这正是下一节"改动要走对比闭环"的实证:删一句、跑一遍、比一次,改动的影响立刻现形,靠肉眼读八段回答永远看不出这一层。

3、评估方法论

框架易写,方法论难守,四条铁律来自大量翻车总结。

第一,单次跑分是玄学:模型输出非确定,同一个八题测试集连跑三遍,分数会在七与八之间跳动;正式结论必须多轮取均值,最好附上波动区间。第二,改动要走对比闭环:删掉提示词里"只输出数字"一句,重跑发现回答里混入废话导致抽取失败——改一行、跑一遍、比一次,三个动作缺一不可。

第三,测试集质量决定评估上限:题目要覆盖边界与陷阱,八道全对只说明"这八道会",不说明强。第四,别让答案泄漏进提示词:把期望输出写进系统提示词"帮"模型,测出来的是复读能力。当这些成为肌肉记忆,提示词工程就从手艺变成了实验科学。

3.1、多轮跑分的口径

"多轮取均值"具体怎么取,实测给个基准:八题测试集连跑五遍,得分依次为 8、7、8、7、8,均值 7.6,波动区间 7 到 8。报告写法是 7.6 ± 0.5(5 轮),均值配区间,别单写一个最大值。跑多少轮合适?经验口径是十轮以内收益明显,再往上均值基本收敛,多跑只是烧钱;两个配置的差值小于波动区间时,只能判"无显著差异",硬解读就是自欺。成本上不用心疼:八题一轮约几千 token,五轮几万 token,几分钱级别——真正贵的是测试集本身的建设。

还有一条分层原则呼应上一篇:检索型 Agent 别只看端到端答案,检索层命中率单独测——五十个问题人工标好应命中的块,检索对不对一眼可判。端到端分数掉了,先分层定位是检索退步还是生成退步,两种修复的成本差一个量级。

4、把两件事接起来

结构化输出与评估是天然搭档:Agent 的产出结构化之后,判分才能自动化;判分自动化之后,提示词、工具描述、模型选型的每次改动才能走"改、测、比"的流水线。本系列前九篇的每个设计决策——工具描述的措辞、压缩参数、检索条数——严格说都应该跑在这个流水线上验收,这也是把评估放在框架对比篇之前的原因:先有标尺,再谈框架谁优谁劣。

串成流水线只比第 2 节多一步:Agent 的最终答案改用 submit_result 提交,判分函数从正则抽取数字换成直接读结构化字段。2.2 节那个"抽取器捞到题干数字"的隐性契约问题从根上消失——判分器不再依赖回答的措辞,只依赖字段的值。这两步合起来,就是"评测驱动开发"的最小可用版:任何改动提交前,先过一遍自己的跑分。

5、总结

结构化输出的手写套路是"唯一工具加 schema 契约",评估的最小闭环是"测试集加自动判分加前后对比";单次跑分不算数、改动必须对比验收、测试集质量是评估上限。对于正在调提示词的同学,建议先花一小时攒十道题的测试集,此后每次改动都跑分记录——一周后你会拥有自己 Agent 的第一条性能曲线。下一篇进入框架的世界:官方 Agent SDK 与手写版逐项对账。评估方法可参考 Anthropic 评估文档。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.3k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
Multi-Agent 详解:多智能体协作架构与实践
精选Agent

Multi-Agent 详解:多智能体协作架构与实践

一个智能体配上一套工具,已经能完成相当多任务。但让它同时“当产品经理、写后端、跑测试、做代码评审”,很快就会露馅:上下文里塞满了互相干扰的指令,十几件工具的清单来回切换,任务做到一半忘了开头。解法看起来很自然——像人类团队一样分工:让多个智能体各司其职,由一个协调者调度,这就是 Multi-Agent(多智能体)系统。它也是 2026 年智能体工程里讨论度最高、误用率也最高的架构。本文按“架构模式…

144