☰
Atomic Agents 结构化 I/O 指南:用 `BaseIOSchema` 为 Agent 定义输入输出契约
2026/10/10 1:31:23 网站建设 项目流程
  • AI Agent
  • Agent 框架
  • MCP 服务
  • 后端

【免费下载链接】atomic-agents

Building AI agents, atomically

项目地址:https://gitcode.com/gh_mirrors/at/atomic-agents
点击查看免费下载

BaseIOSchema是 Atomic Agents 框架中所有 Agent 输入/输出数据结构的统一基类,它把 Pydantic 的强类型校验、强制文档化约束与 Instructor 的 JSON Schema 注入机制无缝衔接。本文以框架官方参考文档claude-plugin/atomic-agents/skills/framework/references/schemas.md为主线,结合仓库源码与测试用例,系统讲解 Schema 的定义规则、字段模式、验证器、组合与错误模式,帮助读者写出能被 LLM 稳定解析、可校验、可复用的结构化数据契约。

一、BaseIOSchema强制执行的规则

BaseIOSchema是一个 PydanticBaseModel子类,并通过元类钩子在类定义时执行检查:如果类没有 docstring,或 docstring 只有空白字符,会立即抛出ValueError。这一设计的目的在于:框架覆写了model_json_schema(),把类 docstring 变成 JSON Schema 的description,类名变成title,而 Instructor 在构造 LLM 提示词时恰好会用到这两项,因此 docstring 必须“写给模型看”,而不只是写给人类看。

from pydantic import Field from atomic_agents import BaseIOSchema class SearchQuery(BaseIOSchema): """Parameters for a web search issued by the agent.""" query: str = Field(..., description="Natural-language search query.") limit: int = Field(default=10, ge=1, le=100, description="Maximum results to return.")

从源码看,atomic-agents/atomic_agents/base/base_io_schema.py中__pydantic_init_subclass__会在每个子类定义完成后调用_validate_description();当 docstring 为空时抛出ValueError(f"{cls.__name__} must have a non-empty docstring ...")。需要注意的是,该方法对 Instructor 内部自动生成的 Schema 做了豁免(通过cls.__module__前缀与from_streaming_response属性判断),以免误伤框架自身的中间类型。

model_json_schema()的重写逻辑也很明确:调用父类生成 schema 后,若description缺失且存在 docstring,则用inspect.cleandoc()清洗缩进后写入description;若title缺失则写入类名。仓库测试atomic-agents/tests/agents/test_atomic_agent.py中的test_base_io_schema_empty_docstring与test_base_io_schema_model_json_schema_no_description分别验证了这两种行为(后者通过 mock 覆盖父类返回空 schema,确认覆写逻辑仍会补充 description)。

docstring 会被传播到哪些地方

BaseIOSchema.model_json_schema()的title/description并不仅仅服务于 Instructor 的提示词构造,它们还参与了框架内部多个核心环节:

  • Prompt 命名:atomic-agents/atomic_agents/base/base_prompt.py中BasePrompt.prompt_name取input_schema.model_json_schema()["title"],prompt_description取["description"],可由BasePromptConfig的title/description覆盖。也就是说,类名与 docstring 直接决定 Prompt 在系统中的展示名与说明。
  • 工具定义:atomic-agents/atomic_agents/agents/atomic_agent.py中_build_tools_definition()在 TOOLS 模式下通过generate_openai_schema(self.output_schema)生成发送给 LLM 的 function schema,_build_schema_for_json_mode()则在 JSON 模式下把model_json_schema()序列化后拼入系统消息。
  • 默认输入输出:框架内置的BasicChatInputSchema/BasicChatOutputSchema(同样定义在atomic_agent.py中)即为BaseIOSchema的子类,分别用 docstring 描述“用户输入”与“Agent 回复”的语义。

二、字段模式:必填、可选、默认值与description

在BaseIOSchema中定义字段的黄金法则是:每个字段都必须带description=,否则 Instructor 没有任何文本可以用来向 LLM 解释该字段的含义,模型只能靠字段名猜测,解析稳定性无从谈起。参考文档给出的完整字段模式如下:

from typing import Optional, Literal from pydantic import Field name: str = Field(..., description="Full legal name.") nickname: Optional[str] = Field(default=None, description="Preferred nickname, if any.") count: int = Field(default=10, ge=1, le=100, description="Items to return (1–100).") sort: Literal["asc", "desc"] = Field(default="desc", description="Sort order.") tags: list[str] = Field(default_factory=list, max_length=10, description="Tag filters (≤10).")

逐项拆解:

  • 必填字段:Field(...)(Ellipsis)表示字段必填,必须提供值才能通过校验,同时强制要求description。
  • 可选字段:Optional[str]搭配default=None,表示该字段可以缺失。若只有Optional[str]而没有默认值,字段会变成“必填但允许为 None”,这通常不是开发者想要的行为(详见“常见错误”一节)。
  • 默认值字段:int = Field(default=10, ge=1, le=100)同时声明默认值、最小值和最大值,LLM 生成的值也会被 Pydantic 校验。ge/le等约束会被写入 JSON Schema,进一步约束模型生成空间。
  • 闭集合:Literal["asc", "desc"]把取值限定在两个字面量上,default="desc"给出默认行为。
  • 列表字段:list[str] = Field(default_factory=list, max_length=10)使用default_factory保证每次实例化都得到新的空列表(避免可变默认值的坑),并用max_length限制元素数量上限。

参考文档特别强调:在闭合取值集合的场景下,优先使用Literal[...]而不是Enum——生成的 JSON Schema 更扁平,更利于 Instructor 处理。当然,Enum在框架中同样受支持(见后文“枚举”小节),两者各有适用场景。

三、验证器:字段级与模型级

字段级验证器

当单个字段需要自定义规则时,使用 Pydantic v2 的field_validator。下面的例子把邮箱地址强制转为小写,并在缺少@时抛出ValueError:

from pydantic import field_validator class EmailSchema(BaseIOSchema): """An email address.""" email: str = Field(..., description="RFC 5322 email address.") @field_validator("email") @classmethod def _lowercase(cls, v: str) -> str: if "@" not in v: raise ValueError("invalid email") return v.lower()

模型级验证器(跨字段)

当校验逻辑依赖多个字段的取值关系时,使用model_validator(mode="after")。下面的DateRange在完整对象构建之后检查日期顺序,保证end不早于start:

from pydantic import model_validator from datetime import date class DateRange(BaseIOSchema): """An inclusive date range.""" start: date = Field(..., description="Start date (inclusive).") end: date = Field(..., description="End date (inclusive).") @model_validator(mode="after") def _ordered(self) -> "DateRange": if self.end < self.start: raise ValueError("end must be on or after start") return self

校验失败后的处理链路

校验失败并不会导致 Agent 直接崩溃。在 Atomic Agents 中,这类错误会转化为Instructor 的重试机制(最多重试max_retries次,模型根据错误信息自行修正输出),同时触发parse:errorhook,开发者可以在 hook 中做日志记录、监控告警或自定义修复逻辑(详见claude-plugin/atomic-agents/skills/framework/references/agents.md与claude-plugin/atomic-agents/skills/framework/references/hooks.md)。

需要特别提醒的是:流式输出时验证器会在字段逐个出现的瞬间触发,因此验证器应保持轻量、幂等,避免昂贵计算——AtomicAgent.run_stream()生成的 partial 对象会随着字段填充反复经过校验。参考文档明确给出了这一约束,仓库atomic_agent.py中run_stream/run_async_stream的实现也印证了“部分字段先填充、逐帧校验”的运行模型。

四、组合、判别联合与枚举

嵌套组合

把子 Schema 作为字段类型即可实现嵌套结构,每个层级都独立享受 docstring 注入与校验:

class Address(BaseIOSchema): """A mailing address.""" street: str = Field(..., description="Street and number.") city: str = Field(..., description="City name.") country: str = Field(..., description="ISO 3166-1 alpha-2 country code.") class Person(BaseIOSchema): """A person with mailing address.""" name: str = Field(..., description="Full name.") address: Address = Field(..., description="Mailing address.")

判别联合(Discriminated Unions)

多态输出是结构化 Agent 的常见需求,例如一条消息既可以是纯文本也可以是图片。参考文档给出的方案是在每个变体上放置一个Literal判别字段:

from typing import Literal, Union class TextPart(BaseIOSchema): """A text message part.""" kind: Literal["text"] = "text" text: str = Field(..., description="Plain-text body.") class ImagePart(BaseIOSchema): """An image attachment.""" kind: Literal["image"] = "image" url: str = Field(..., description="Publicly accessible image URL.") class Message(BaseIOSchema): """A multimodal message part.""" part: Union[TextPart, ImagePart] = Field(..., description="Message content.")

kind字段同时承担“判别标签”与“自我描述”双重职责:LLM 只需选择text或image,Pydantic 即可据此路由到正确的变体结构。这种模式也是后续“错误 Schema 模式”的基础。

枚举

当需要命名的固定取值集合时,使用继承str的Enum,让取值既是成员名也是可序列化的字符串值:

from enum import Enum class Priority(str, Enum): LOW = "low" MEDIUM = "medium" HIGH = "high" class Task(BaseIOSchema): """A unit of work.""" title: str = Field(..., description="Task title.") priority: Priority = Field(default=Priority.MEDIUM, description="Priority level.")

关于枚举有一个来自源码测试的重要细节:Instructor 默认strict=True,这会导致枚举字段只能收到枚举实例、阻止 Pydantic 从字符串做常规强转。AtomicAgent._get_completion_kwargs()特意把strict默认值设为None,让output_schema自身的 Pydantic 行为生效。atomic-agents/tests/agents/test_atomic_agent.py中的test_run_uses_pydantic_default_strictness_for_enum_output验证了默认情况下"food"字符串可以被正确转换为Topic.FOOD枚举实例;而test_run_respects_explicit_strict_override_for_enum_output则验证了当用户在model_api_parameters中显式传入strict=True时,字符串强转会被拒绝并抛出ValidationError。这意味着:在 Agent 中定义枚举字段时,默认宽松转换即可正常工作,无需为兼容性做额外处理。

五、错误 Schema 模式:用结构化替代异常

当工具或 Agent 存在“合法失败”的可能性时,参考文档建议不要抛异常,而是把失败建模为结构化的替代输出。两种常见的形态:

形态一:成功/失败成对 Schema,通过输出联合返回

适合调用方需要穷尽处理每一种情况的场景(例如网关、任务编排器):

class SearchSuccess(BaseIOSchema): """Successful search result.""" results: list[str] = Field(..., description="Matching items.") class SearchFailure(BaseIOSchema): """Search could not complete.""" error: str = Field(..., description="Human-readable failure reason.") code: Literal["rate_limited", "no_results", "upstream_error"] = Field( ..., description="Machine-readable failure code." ) class SearchOutput(BaseIOSchema): """Search output — either success or typed failure.""" result: Union[SearchSuccess, SearchFailure] = Field(..., description="Outcome.")

code字段使用Literal限定了机器可读的错误码集合,下游调用方可以据此做精确的分支路由;error提供面向用户的可读原因。

形态二:单一 Schema 上的判别状态字段

适合大多数代码路径只关心status == "ok"的场景(例如日志、简单查询包装):

class SearchOutput(BaseIOSchema): """Search result envelope.""" status: Literal["ok", "error"] = Field(..., description="Outcome code.") results: list[str] = Field(default_factory=list, description="Items when status='ok'.") error: Optional[str] = Field(default=None, description="Message when status='error'.")

两种形态的选择依据很直白:需要穷尽分支用联合 Schema,仅需快速判断成败用状态字段。相比抛出异常,这种建模让 Agent 的输出契约自包含错误语义,LLM 更容易学会“失败也是一种合法答案”,同时 Pydantic 仍然全程参与校验。

六、常见错误清单

参考文档列出的五个高频错误,每一类都能在仓库源码或测试中找到对应的反例与后果:

  1. 忘记 docstring:框架在类定义导入时直接抛出ValueError("... must have a non-empty docstring ..."),test_base_io_schema_empty_docstring就是这一行为的回归测试——空 docstring("""""")的类定义会在with pytest.raises上下文中被立即拒绝。

  2. 使用普通BaseModel而非BaseIOSchema:会同时失去“docstring 强制检查”和“model_json_schema()覆写”两层保障。更重要的是,AtomicAgent/BasePrompt/BaseTool的泛型参数要求 Schema 必须是BaseIOSchema子类(参见atomic-agents/atomic_agents/agents/atomic_agent.py的类型约束),传入裸BaseModel会破坏整个结构化链路。

  3. Field()不带description=:Instructor 就没有任何文本可以向 LLM 说明该字段的含义,模型只能凭字段名与类型猜测,输出命中率大幅下降。这是所有字段模式中最容易忽视、影响却最直接的一点。

  4. Optional[str]没有默认值:字段会被判定为“必填但允许为 None”——请求时既不能省略、值又可能是None,几乎总与开发意图相悖。正确写法是Optional[str] = Field(default=None, ...)。

  5. 过度宽泛的类型(dict、Any、object):LLM 会自由生成任意结构,Pydantic 无法对其做有效校验,Schema 退化为“没有 Schema”。应尽量拆分为具名字段、嵌套结构或Literal限定集合。

七、落地实践:从 Schema 到可运行 Agent

最后以一个完整的运行链路收尾:参考文档中定义的SearchQuery这样的BaseIOSchema,在真实项目中会同时充当 Agent 的输入与输出契约。仓库示例atomic-examples/quickstart/quickstart/2_basic_custom_chatbot.py展示了标准用法——先构造AgentConfig(client为instructor.from_openai(...)包装的客户端),再用泛型AtomicAgent[InputSchema, OutputSchema]声明类型,运行时通过agent.input_schema(chat_message=...)实例化输入,agent.run()返回的即是对应输出 Schema 的实例,可直接用model_dump()序列化。而BaseIOSchema.__str__与__rich__的实现(见atomic-agents/atomic_agents/base/base_io_schema.py)分别把实例转为 JSON 字符串与 Rich 可渲染的 JSON 对象,方便调试输出与终端展示——test_base_agent_io_str_and_rich即验证了str()输出与model_dump_json()一致。

小结

BaseIOSchema是 Atomic Agents 中“原子化”思想的直接载体:每一个输入输出都被定义为一个自带文档、自带校验、自带 JSON Schema 导出能力的 Pydantic 模型。理解并遵守其规则——docstring 必填且写给模型、字段必带description、优先Literal与嵌套组合、用结构化错误替代异常——就能让 Agent 的每个接口都稳定、可测、可被 LLM 精确理解。本文涉及的核心源码与测试均可直接在仓库中查阅:BaseIOSchema 实现、Agent 与内置 Schema、BasePrompt 的 Schema 复用、Schema 相关测试,以及完整的 Quickstart 可运行示例。

  • AI Agent
  • Agent 框架
  • MCP 服务
  • 后端

【免费下载链接】atomic-agents

Building AI agents, atomically

项目地址:https://gitcode.com/gh_mirrors/at/atomic-agents
点击查看免费下载

相关推荐

上一篇:解决Layui 2.9.17与jQuery 3.7.1兼容性问题的完美指南
下一篇:FlowMVI与Essenty/Decompose集成:构建可维护的大型应用架构的终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询