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_retries、retry_delay、 _get_retryable_exceptions()),遇到可重试异常会自动重试。
  • 它是 ChatModelBase 的方法,所有 chat model 子类(DashScopeChatModel、OpenAIChatModel、 AnthropicChatModel 等)都继承该方法。若底层 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

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

与 Agent 的关系 ​

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

  • 它直接在 model 上调用,绕过 Agent 的 ReAct 循环、工具编排、权限拦截与上下文管理。
  • 它适合"我只需要一次结构化提取,不需要 Agent 自主决策"的场景,例如从一段文本抽取实体、分类、 生成配置对象。
  • Agent.reply() / Agent.reply_stream() 返回的是 Msg(含 TextBlock、ToolCallBlock 等 内容块),不会直接返回结构化 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 calling:generate_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 在回复前显式产出与修订任务计划

官方参考 ​

学习文档整合站点