Skip to content

结构化输出

让模型返回一段自由文本很容易,让模型返回一个字段、类型、嵌套都严格符合预期 schema 的结构化对象 却很难。AgentScope 2.x 把这件事放在 model 层ChatModelBase.generate_structured_output 负责把 schema 翻译成一次强制工具调用,再校验、修复模型产出,最终返回一个已通过校验的 dict。

版本基线:本文以 Python agentscope 2.0.4(PyPI,2026-07-07)为准,要求 Python >=3.11。 结构化输出是 model 层方法,定义在 ChatModelBase 上(源:src/agentscope/model/_base.py), 导入路径为 from agentscope.model import DashScopeChatModel, StructuredResponse。2.x 文档站存在 版本化陷阱:非版本化的 /en/ URL 会 404,本页所有官方链接均使用 versions/2.0.4/en/ 形式。

什么是结构化输出

结构化输出(structured output)是指:给定一个 schema,要求模型生成的结果严格符合该 schema, 而不是返回一段需要你再用正则或 json.loads 解析的自由文本。AgentScope 2.x 的 generate_structured_output 返回一个 StructuredResponse,其 content 字段是一个已通过校验 的 dict,字段名、类型、必填项都和 schema 对齐,你可以直接当字典使用,无需再做容错解析。

前端类比

如果你来自前端,可以类比为 TypeScript 类型 + JSON.parse + zod 校验的组合:

  • 你先用 TypeScript 接口(或 zod schema)声明期望的形状。
  • 模型返回的是一段"像 JSON 的文本"。
  • 框架帮你 JSON.parse,再用 schema 校验,校验失败时尝试修复。
  • 最终你拿到一个类型安全、可直接访问字段的对象。

对应到 AgentScope:Pydantic BaseModel 扮演 zod schema 的角色,generate_structured_output 扮演 "解析 + 校验 + 修复"的运行时,StructuredResponse.content 扮演校验通过后的对象。

AgentScope 原生语义:结构化输出不是 Agent 层能力,而是 model 层能力。它的实现方式是把 schema 合成为一个名为 generate_structured_output 的工具,用 tool_choice 强制模型调用该工具,再从 工具调用的参数中取出 JSON、修复解析、用 schema 校验。这意味着所选模型必须支持 function calling / tool calling,否则强制工具调用会失败。它和"让模型直接输出 JSON 文本再解析"有本质区别:前者复用了 模型已有的工具调用能力,后者依赖模型的 JSON 格式遵从性。

核心 API

结构化输出的入口是 ChatModelBase.generate_structured_output不是 Agent 层方法:

python
async def generate_structured_output(
    self,
    messages: list[Msg],
    structured_model: Type[BaseModel] | dict,
    **kwargs: Any,
) -> StructuredResponse
参数类型说明
messageslist[Msg]提供给模型的上下文,不能为空
structured_modelType[BaseModel] | dictPydantic BaseModel 子类,或一个 JSON Schema dict(源:_base.py 的类型签名与 docstring)
**kwargsAny透传给底层 _call_api,例如 tool_choice

返回值是 StructuredResponse,其 .content 是已校验的 dict。

关键事实(源码核验,src/agentscope/model/_base.py):

  • structured_model 同时接受 Pydantic BaseModel 子类 JSON Schema dict。若传入 dict, 直接作为 JSON Schema 使用;若传入 Pydantic 类,调用 .model_json_schema() 转成 JSON Schema。
  • 该方法与 __call__ 共享同一套重试设置max_retriesretry_delay_get_retryable_exceptions()),遇到可重试异常会自动重试。
  • 它是 ChatModelBase 的方法,所有 chat model 子类(DashScopeChatModelOpenAIChatModelAnthropicChatModel 等)都继承该方法。若底层 API 原生支持结构化输出,子类可覆盖 _call_api_with_structured_output 提供更精确的实现。

完整示例

下面用 DashScopeChatModel 让模型返回一个符合 WeatherInfo schema 的结构化对象。密钥通过环境变量 注入,避免硬编码。

python
import asyncio
import os

from pydantic import BaseModel

from agentscope.model import DashScopeChatModel
from agentscope.credential import DashScopeCredential
from agentscope.message import UserMsg


class WeatherInfo(BaseModel):
    city: str
    temperature: float
    unit: str


async def main() -> None:
    model = DashScopeChatModel(
        credential=DashScopeCredential(api_key=os.environ["DASHSCOPE_API_KEY"]),
        model="qwen-plus",
        stream=False,
    )

    response = await model.generate_structured_output(
        messages=[
            UserMsg(name="user", content="What's the weather in Shanghai?"),
        ],
        structured_model=WeatherInfo,
    )

    # response.content 是已校验的 dict,字段与 WeatherInfo 对齐
    print(response.content)
    # 例: {'city': 'Shanghai', 'temperature': 26.5, 'unit': '°C'}

    # 可以直接按字段访问
    info = response.content
    print(f"{info['city']}: {info['temperature']}{info['unit']}")


asyncio.run(main())

如果不想定义 Pydantic 类,也可以直接传一个 JSON Schema dict(源码已确认支持):

python
weather_schema = {
    "type": "object",
    "properties": {
        "city": {"type": "string"},
        "temperature": {"type": "number"},
        "unit": {"type": "string"},
    },
    "required": ["city", "temperature", "unit"],
}

response = await model.generate_structured_output(
    messages=[UserMsg(name="user", content="What's the weather in Shanghai?")],
    structured_model=weather_schema,
)

两种形式的区别仅在校验方式:Pydantic 类走 model_validate,dict 走 jsonschema.validate。 最终 response.content 都是 dict。

原理:forced tool call

generate_structured_output 不是一个独立的模型 API,而是把结构化输出问题转化为一次强制工具 调用。下面的步骤对应 _call_api_with_structured_output 的源码逻辑:

schema (Pydantic / dict)

  ├─ 若是 Pydantic → model_json_schema() 转成 JSON Schema
  │  若是 dict    → 直接作为 JSON Schema

合成名为 "generate_structured_output" 的 function tool
(parameters = 上面的 JSON Schema)

  ├─ tool_choice 默认 = ToolChoice(mode="generate_structured_output")
  │  即强制模型必须调用这个工具

在最后一条 user 消息后追加指令:
  "Now you **MUST** call the tool named 'generate_structured_output' ..."


调用底层 _call_api(带 tools + tool_choice)


从响应的 ToolCallBlock 中取出 name == "generate_structured_output" 的工具调用

  ├─ 用 _json_loads_with_repair 解析工具参数(含 JSON 修复)

校验:
  dict    → jsonschema.validate(output, schema)
  Pydantic→ ModelClass.model_validate(output)

  ├─ 校验失败 → 抛出异常(可重试异常会触发重试)

返回 StructuredResponse(content=已校验 dict, ...)

几个要点:

  • 强制工具调用是核心机制:默认 tool_choice 指向合成工具的名字,模型被强制调用它。如果模型 不支持 tool calling,或 API 拒绝强制 tool_choice,这一步会失败。
  • JSON 修复_json_loads_with_repair 会在模型产出的工具参数不是合法 JSON 时尝试修复,降低 因格式瑕疵导致的失败率。但修复只针对 JSON 解析,schema 校验失败不会自动修复,而是抛出异常。
  • 可降级的 tool_choice:某些 API 在特定模式下拒绝强制 tool_choice(例如 DashScope 在 thinking 模式下)。源码注释说明,这时可以通过 **kwargs 传入 tool_choice=ToolChoice(mode="auto"),放弃 强制、仅靠注入的指令提示模型调用工具。但这依赖模型遵从指令,不保证成功。
  • 流式积累:当 stream=True 时,方法内部会自行累积流式 chunk,直到拿到 is_last=True 的完整 响应,再提取工具调用。你不需要手动处理流。

StructuredResponse 结构

generate_structured_output 返回 StructuredResponse(从 agentscope.model 导入)。源码构造逻辑 如下:

字段类型说明
contentdict[str, Any]已通过 schema 校验的结构化输出,直接当字典使用
idstr继承自底层 ChatResponse 的请求 ID
created_atstr继承自底层 ChatResponse 的创建时间戳
usageChatUsage | Nonetoken 用量与耗时,继承自底层响应
finished_reasonFinishedReason通常为 FinishedReason.COMPLETED;中断时为 INTERRUPTED
python
from agentscope.model import StructuredResponse

response: StructuredResponse = await model.generate_structured_output(
    messages=[...],
    structured_model=WeatherInfo,
)

response.content          # {'city': 'Shanghai', 'temperature': 26.5, 'unit': '°C'}
response.usage            # ChatUsage(input_tokens=..., output_tokens=..., time=...)
response.finished_reason  # FinishedReason.COMPLETED

注意 contentdict 而非 Pydantic 实例。即使你传入 WeatherInfo 类,response.content 也是 dict,校验由内部 model_validate 完成。如果你需要 Pydantic 实例,可以自行二次构造: WeatherInfo(**response.content)

与 Agent 的关系

这是一个容易混淆的边界问题。generate_structured_outputChatModelBase 的方法,属于 model 层,不是 Agent 的方法。这意味着:

  • 它直接在 model 上调用,绕过 Agent 的 ReAct 循环、工具编排、权限拦截与上下文管理。
  • 它适合"我只需要一次结构化提取,不需要 Agent 自主决策"的场景,例如从一段文本抽取实体、分类、 生成配置对象。
  • Agent.reply() / Agent.reply_stream() 返回的是 Msg(含 TextBlockToolCallBlock 等 内容块),不会直接返回结构化 dict。

未确认:截至 2.0.4 文档,Agent 层没有提供"在 reply 中强制返回结构化对象"的便捷方法。 官方文档与源码的 Agent 类均未暴露类似 agent.reply_structured 的接口。如果你需要在 Agent 回复中强制结构化输出,当前可能需要借助 middleware 注入 system prompt、或在 Agent 外层用 generate_structured_output 二次处理 reply 的结果。这部分属于未由文档覆盖的自行组装,接入前 请跟踪 release notes 确认是否有官方方案。

换句话说:一次性结构化提取用 model 层方法;Agent 自主任务中的结构化约束需要自行组装,两者不 要混用。

常见错误

  • schema 字段无类型注解:Pydantic BaseModel 的字段必须有类型注解(如 city: str)。漏写 类型会导致 model_json_schema() 产出不完整的 schema,模型无法理解字段约束。
  • 模型不支持 tool callinggenerate_structured_output 的底层是强制工具调用。如果所选模型 不支持 function calling,强制 tool_choice 会失败,或模型直接产出文本而忽略工具调用,最终在 "找不到 generate_structured_output 工具调用"处抛出 RuntimeError。AgentScope 的 ChatModelBase 子类要求模型原生支持 tool calling。
  • 误以为 agent.reply 直接返回结构化对象reply() 返回 Msg,不是 dict。结构化输出是 model 层方法,与 Agent 的 reply 是两条独立路径。
  • 校验失败期望自动修复_json_loads_with_repair 只修复 JSON 解析(如尾逗号、缺引号), 不修复 schema 语义错误(如字段类型不对、缺必填项)。后者会直接抛出 ValidationError, 靠 max_retries 重试。
  • 在 thinking 模式下强制 tool_choice:DashScope 等提供商在 thinking 模式下可能拒绝强制 tool_choice。如源码注释所述,这时需传入 tool_choice=ToolChoice(mode="auto") 降级为指令提示, 但成功率依赖模型遵从性。
  • content 当 Pydantic 实例用response.content 永远是 dict。直接 response.content.city 会抛 AttributeError,应该用 response.content["city"]

先修

  • 工具与 MCP - 理解工具调用机制,结构化输出底层依赖 forced tool call

下一步

  • 计划模式 - 让 Agent 在回复前显式产出与修订任务计划

官方参考

学习文档整合站点