跳转至

结构化输出

给 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,见 工具设置。