恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

049、使用Pydantic定义Agent数据结构

  • 首页
  • 资讯中心
  • /
  • 049、使用Pydantic定义Agent数据结构

相关资讯

DLL Sideloading 攻击检测实战指南:基于 Sysmon 与 MITRE T1574 的威胁狩猎方法论 2026/9/13 2:05:54
Cloudflare Turnstile 配置完全指南:脚本加载、组件选项与框架集成的实战手册(Codex Skills 仓库深度解析) 2026/9/13 2:05:54
ESP32-P4 USB Host实战:从枚举到FatFs的U盘读写完整指南 2026/9/13 2:00:54

最新资讯

深入掌握 lo 库的 AttemptWhile:基于 Go 泛型的可控重试机制
AI模型管理与生产部署实战指南
AI Agent跨会话记忆系统实战:从存储到认知的范式重构
AI对话生成数据表与自动化工作流:零代码平台实战全攻略
如何确保 Claude Code 在复杂任务上总是自动触发 planning-with-files 技能
如何把 Bun.serve() 应用部署到 Vercel 并配置 bunVersion

今日推荐

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验
Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化
Flutter应用改名全指南:从Android到iOS的配置与工具实践

本周热门

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验
Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化
Flutter应用改名全指南:从Android到iOS的配置与工具实践

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

049、使用Pydantic定义Agent数据结构

发布时间:2026/9/13 2:05:54
049、使用Pydantic定义Agent数据结构 049、使用Pydantic定义Agent数据结构昨天调一个Agent的tool callingJSON Schema校验死活过不去。我让模型输出一个包含“天气查询”参数的调用结果它把temperature传成了字符串而我的函数签名里写的是float。最气人的是报错信息不是“类型错误”而是“Field required”——因为我在Pydantic模型里用了Optional[float]但没给默认值。那一刻我才意识到把Agent的输入输出结构交给Pydantic不只是为了“好看”而是为了让边界条件在运行前就暴露出来。很多人写Agent数据结构用的是裸字典defrun_agent(user_input:str)-dict:messages[{role:user,content:user_input}]responsellm.chat(messages)return{reply:response,tool_calls:response.tool_calls}看起来没问题但一旦Agent开始调用多个工具返回的字典里可能缺tool_calls键也可能多出metadata。你只能靠if tool_calls in result这种防御式写法一层层包。这不是写代码这是打地鼠。我更习惯把Agent的输入、中间状态、最终输出全部定义成Pydantic模型。Agent本质上是一个有状态的对象状态需要被约束。比如一个典型的ReAct Agent它的思考记录长这样frompydanticimportBaseModel,FieldfromtypingimportLiteral,Union,ListclassThought(BaseModel):step:intField(...,description当前推理步数从0开始)content:strField(...,description模型生成的思考文本)action:Union[Literal[search,calculator,sql_query],None]Field(defaultNone,description要调用的工具名如果不调用则为None)action_input:Union[dict,None]Field(defaultNone,description传给工具的参数必须是dict)这里action_input我故意设计成dict而不是具体模型是因为不同工具的参数千差万别。但如果你在写一个垂直Agent比如只做SQL查询那action_input最好用具体模型classSQLQueryInput(BaseModel):sql:strField(...,descriptionSQL语句)limit:intField(5,ge1,le100,description返回行数默认5)classSQLThought(Thought):action:Literal[sql_query]sql_queryaction_input:SQLQueryInput这样写有个巨大的好处当LLM返回的JSON里limit是“50”字符串Pydantic会自动尝试类型转换。如果值超出1到100它立刻抛异常。你能在日志里看到是哪个字段、哪个值、为什么失败。而不是等到SQL执行到一半才报错。但别以为Pydantic会自动把所有字符串转成数字。默认情况下int字段接收字符串“50”确实能转成功但float字段接收字符串“3.14”也能转。可如果字符串是“abc”它就抛错。问题是我们经常会遇到模型输出action_input: {limit: 五十}这种诡异内容Pydantic直接抛ValidationError你的Agent循环必须捕获这个异常然后把它作为错误信息回传给LLM让它重新生成。这个闭环设计是Agent稳定性的关键。我曾经犯过一个错误把所有的工具调用结果都塞进一个List[dict]然后存进ConversationHistory。后来需要统计某个工具的平均耗时只能遍历字典判断tool_name。这太蠢了。应该直接用Pydantic的Field或者discriminator做多态。比如classToolCallRecord(BaseModel):tool_name:strduration_ms:intargs:dictclassSearchRecord(ToolCallRecord):tool_name:Literal[search]query:strresults_count:intclassCalculatorRecord(ToolCallRecord):tool_name:Literal[calculator]expression:strresult:float然后用带discriminator的模型frompydanticimportTaggedUnionclassAgentStep(BaseModel):step_id:intrecord:TaggedUnion({search:SearchRecord,calculator:CalculatorRecord})这样当你拿到AgentStep实例时record已经是具体的SearchRecord或CalculatorRecord直接用record.query访问不用再做类型判断。这里踩过坑TaggedUnion的写法在Pydantic V2里和V1不一样V1用Field(discriminatortool_name)V2我用TaggedUnion但更推荐直接把这些模型放进Union里然后让Pydantic自动判别。具体看你的版本别盲目抄网上的旧代码。回到Agent本身。Agent的状态循环里最需要被定义的数据结构是“当前步骤的结果”。我通常这样写classAgentState(BaseModel):task:strstep_index:int0max_steps:int10thoughts:List[Thought]Field(default_factorylist)final_answer:Union[str,None]Noneerror:Union[str,None]None然后Agent的每一步都基于这个state更新而不是直接改字典。更新时用model_copy(update...)保持不可变性方便追踪statestate.model_copy(update{step_index:state.step_index1,thoughts:state.thoughts[new_thought]})这里有个细节thoughts是列表model_copy(update...)不会深拷贝所以如果你把new_thought直接追加到原列表再赋给新state可能会影响旧state的引用。用state.thoughts [new_thought]创建新列表原state就不受污染。这个坑我踩了两次一次在Agent重试逻辑里一次在写单元测试时。再聊聊LLM输出解析。现在主流模型都支持response_format{type: json_object}但返回的JSON有时候不是标准JSON比如带三引号注释或者单引号。Pydantic本身不负责解析JSON字符串你得先json.loads再Model.model_validate_json。我建议直接用model_validate_json它比json.loads更宽容能处理一些编码问题try:thoughtThought.model_validate_json(raw_llm_output)exceptValidationErrorase:error_payload{role:system,content:fJSON parse error:{e}. Please fix your output.}messages.append(error_payload)# 重新调用LLM最多重试3次这里别写except Exception——你会把KeyboardInterrupt也吞了。精确捕获ValidationError然后针对错误内容做提示。LLM看到错误提示后大概率能自我纠正。如果你发现模型总是错在同一个字段比如action拼写为search_web那就是你的工具名设计有歧义要在prompt里强调可用值列表或者用Literal限制后把错误信息中的“enum”提示一起回传。还有一类坑是Optional与Union混用。很多人写classToolResult(BaseModel):data:Union[str,dict,list]|NoneNone这等于没有约束。我更建议用Any或JsonValue显式说明“这里放任意JSON类型”但别用裸Optional。如果某个字段是可选的一定要想清楚是“缺少”还是“值为空”classAgentResponse(BaseModel):reply:strField(...,descriptionAI的回复文本)tool_used:Union[str,None]Field(defaultNone,description使用的工具名没有则为None)tool_result:Union[str,None]Field(defaultNone,description工具的原始输出没有则为None)当tool_usedNone时前端就能知道这次回复没有工具调用而不是靠判断tool_result是否为空字符串。最后分享一个经验别把Pydantic模型当作数据库ORM来用。Pydantic只负责运行时数据校验不负责持久化。我在项目里见过同事把整个对话历史塞进一个Pydantic模型然后直接存进MongoDB导致字段冗余且迁移困难。正确做法是用Pydantic定义API边界的数据结构内部存储用普通字典或者专用ORM。Agent的工具调用参数用Pydantic校验校验通过后再传给真实的函数。这相当于在LLM和你自己写的代码之间加了一道严防死守的防线。写这套系列文章时我一直在试一个本地Agent框架。每次让LLM输出函数调用参数我就在想如果这个世界没有Pydantic我得写多少层if type(x) str: x float(x)。现在这些校验代码全被Pydantic的报错信息替代了。花点时间把Agent的状态模型、工具参数模型定义清楚后面调试Agent行为时你会感谢当初那个愿意多写几行类的自己。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号