Appearance
结构化输出
让模型返回一段自由文本很容易,让模型返回一个字段、类型、嵌套都严格符合预期 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| 参数 | 类型 | 说明 |
|---|---|---|
messages | list[Msg] | 提供给模型的上下文,不能为空 |
structured_model | Type[BaseModel] | dict | Pydantic BaseModel 子类,或一个 JSON Schema dict(源:_base.py 的类型签名与 docstring) |
**kwargs | Any | 透传给底层 _call_api,例如 tool_choice |
返回值是 StructuredResponse,其 .content 是已校验的 dict。
关键事实(源码核验,src/agentscope/model/_base.py):
structured_model同时接受 PydanticBaseModel子类和 JSON Schemadict。若传入 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 导入)。源码构造逻辑 如下:
| 字段 | 类型 | 说明 |
|---|---|---|
content | dict[str, Any] | 已通过 schema 校验的结构化输出,直接当字典使用 |
id | str | 继承自底层 ChatResponse 的请求 ID |
created_at | str | 继承自底层 ChatResponse 的创建时间戳 |
usage | ChatUsage | None | token 用量与耗时,继承自底层响应 |
finished_reason | FinishedReason | 通常为 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 在回复前显式产出与修订任务计划