结构化输出¶
给 execute() 传一个 pydantic 模型,返回值就会是被验证过的模型实例:
from pydantic import BaseModel
from xun import Agent
class StoryModel(BaseModel):
title: str
content: str
story = (
Agent()
.initialize()
.instruct("Write a short story within 100 words.")
.execute(schema=StoryModel) # Result[StoryModel, ErrorInfo]
.unwrap() # StoryModel
)
print(f"Title: {story.title}")
先有用户消息
Conversation.set_response_schema() 把 schema 挂在末尾的用户消息上,
没有用户消息时直接 ValueError: No user message to attach a response schema to.。
所以 instruct() 必须在 execute(schema=…) 之前。
三层约束¶
flowchart TD
A["execute(schema=M)"] --> L1["提示注入<br/>把 JSON Schema 附在用户消息后"]
L1 --> L2{"本轮注册了工具?"}
L2 -- 否 --> P["再交给服务端 response_format<br/>strict=False"]
L2 -- 是 --> S["只靠提示注入"]
P --> R["循环结束"]
S --> R
R --> V["json_repair 修复 + M.model_validate"]
V -- 失败 --> E["ErrorEvent + Result.Err(ErrorInfo)"]
V -- 通过 --> OK["Result.Ok(M 实例)"]
| 层 | 强度 | 说明 |
|---|---|---|
| 提示注入 | 始终生效 | 用户消息后附一段提示与 model_json_schema() 的输出 |
response_format |
软约束 | 只有在没有注册工具时才设置;带工具时强制 JSON 会让模型跳过工具调用。即使设置也是 strict: False |
| 事后校验 | 硬约束 | json_repair.loads 先修 JSON,再 model_validate;失败即发 ErrorEvent 并抛出,最终以 Result.Err 呈现 |
第二层是刻意做软的:一旦有工具可用,response_format 与工具调用会互相干扰,
框架选择让提示注入承担主要约束,把「一定合规」交给第三层的校验。
校验失败会怎样¶
校验失败不会静默降级成字符串:
res = agent.instruct(...).execute(schema=StoryModel)
if res.is_err():
print(res.unwrap_err().error) # 形如 Failed to parse result into <class 'StoryModel'>: ...
想重试就再 execute() 一次,或者放宽模型定义。
与钩子一起用¶
before_execution 钩子拿到的是执行参数本身,可以就地改 schema 或 max_iterations,
也可以设置 takeover_result 直接接管本轮执行:
from xun import HookArgs
def stub(args: HookArgs.BeforeExecutionArgs) -> None:
if args.schema is not None:
args.takeover_result = '{"title": "stub", "content": "…"}'
agent.hooks.before_execution.add(stub)
设置了 takeover_result 后:不再调用模型、不再触发执行内部的其他钩子与事件、不写入会话历史;
但仍然按 schema 校验,非法内容会让本轮失败。
工具返回值不是结构化输出¶
execute(schema=…) 约束的是这一轮结束时的最终回答。工具自己的返回值走的是另一条路:
返回值必须是 JSON 可序列化的,异常被 except_safe 包成 Result.Err,见 工具设置。