Crew ai 源码拆解-Day 4: tools/structured_tool.py — 工具运行时包装 Day 4: tools/structured_tool.py — 工具运行时包装这个文件负责什么一句话CrewStructuredTool 是工具在框架内部运行时的真实形态BaseTool/Tool 最终都会转换成它来执行。源码位置d:\crewAI\lib\crewai\src\crewai\tools\structured_tool.py328 行代码。在 CrewAI 中的位置你写的工具 框架内部运行时 │ │ ├── tool 装饰器 │ │ def search(): ... │ │ │ │ ↓ Day 3 学的 │ │ │ ├── Tool 对象 │ │ (base_tool.py) │ │ │ │ ↓ to_structured_tool() │ │ │ ├── CrewStructuredTool ←──── 本文件 │ (structured_tool.py) │ │ │ │ ↓ invoke() 执行 │ │ │ └── 返回结果给 Executor │关键转换# Day 3 学的 Tool 对象 tool Tool(namesearch, funcsearch, ...) # 转换成 CrewStructuredTool框架内部用 structured tool.to_structured_tool() # 执行 structured.invoke({query: AI})核心类CrewStructuredToolclass CrewStructuredTool(BaseModel):大白话把工具包装成一个标准化的执行器让框架内部统一调用。核心字段字段类型含义namestr工具名descriptionstr工具描述args_schemaPydantic Model参数格式可序列化/反序列化funcCallable包装的函数result_as_answerbool结果是否直接作为最终答案max_usage_countint最大使用次数current_usage_countint已使用次数cache_functionCallable缓存判断函数核心方法1. from_function() — 从函数创建工具类方法classmethod def from_function(cls, func, nameNone, descriptionNone, ...) - CrewStructuredTool:作用从一个普通函数创建 CrewStructuredTool。def add(a: int, b: int) - int: 两数相加 return a b tool CrewStructuredTool.from_function(add) # name add # description 两数相加 # args_schema 自动从函数签名生成内部做了什么读取函数名 →name读取 docstring →description从函数签名生成 args_schema → Pydantic 模型包装函数 →func2. invoke() — 执行工具最核心def invoke(self, input: str | dict, configNone, **kwargs) - Any:执行流程invoke({query: AI}) ↓ 1. _parse_args(input) → 解析参数字符串JSON→字典→验证 ↓ 2. has_reached_max_usage_count() → 检查使用次数 ↓ 3. _increment_usage_count() → 增加计数 ↓ 4. self.func(**parsed_args) → 执行实际函数 ↓ 5. 返回结果3. _parse_args() — 参数解析def _parse_args(self, raw_args: str | dict) - dict:处理三种输入字符串 JSON → json.loads() → 字典 → model_validate() → 验证后的字典 字典 → model_validate() → 验证后的字典 无 schema → 直接返回原始字典4. ainvoke() — 异步执行async def ainvoke(self, input, configNone, **kwargs) - Any:和 invoke() 的区别如果函数是异步的用await执行如果是同步的用run_in_executor在线程池中执行。5. _increment_usage_count() — 使用计数def _increment_usage_count(self): self.current_usage_count 1 if self._original_tool is not None: self._original_tool.current_usage_count self.current_usage_count注意不仅更新自己的计数还同步更新原始 Tool 对象的计数。执行流程图Mermaidgraph TD A[调用 invoke#40;input#41;] -- B{input 类型?} B --|字符串 JSON| C[json.loads 解析] B --|字典| D[直接使用] C -- D D -- E[_parse_args 验证参数] E -- F{args_schema 存在?} F --|是| G[model_validate 验证] F --|否| H[直接返回字典] G -- I[验证后的参数] I -- J[has_reached_max_usage_count] J --|超过限制| K[抛出异常] J --|通过| L[_increment_usage_count] L -- M{func 是异步?} M --|是| N[await func] M --|否| O[func 同步执行] N -- P[返回结果] O -- P style A fill:#e1f5fe style E fill:#fff3e0 style J fill:#fce4ec style P fill:#e8f5e9BaseTool vs CrewStructuredTool 对比特性BaseTool/ToolCrewStructuredTool文件base_tool.pystructured_tool.py用途用户创建工具框架内部运行时创建方式tool 装饰器from_function() 或 to_structured_tool()执行方法run()invoke()参数输入关键字参数字典或 JSON 字符串参数验证_validate_kwargs()_parse_args()异常处理返回错误信息抛出异常使用场景用户代码Executor 内部调用一句话BaseTool 是给用户用的友好接口CrewStructuredTool 是给框架用的标准接口。我应该重点看哪几行代码行号内容为什么重要L68class CrewStructuredTool(BaseModel)类定义L77-88核心字段name/description/args_schema/funcL98-154from_function()从函数创建工具的类方法L212-234_parse_args()参数解析和验证L280-304invoke()最核心执行工具的主方法你在自己的项目里怎么用你不需要直接创建 CrewStructuredTool。框架会自动帮你转换。# 你的项目: src/my_project/tools/custom_tool.py from crewai.tools import tool tool def search(query: str) - str: 搜索网页 return f搜索结果: {query} # 当 Agent 使用这个工具时框架内部自动执行 # 1. Tool 对象你看到的 # 2. → to_structured_tool() 转换 # 3. → CrewStructuredTool.invoke() 执行你只需要写 tool框架自动处理后面的转换和执行。阅读后你应该理解什么[ ] CrewStructuredTool 是工具的运行时形态[ ] BaseTool/Tool 通过to_structured_tool()转换成 CrewStructuredTool[ ]from_function()从函数创建 CrewStructuredTool[ ]invoke()是执行工具的主方法解析参数→检查计数→执行函数[ ]_parse_args()处理字符串 JSON 和字典两种输入[ ] CrewStructuredTool 用字典输入BaseTool 用关键字参数输入[ ] 异常处理不同CrewStructuredTool 抛异常BaseTool 返回错误信息[ ] 用户不需要直接使用 CrewStructuredTool框架自动转换5个检查问题Q1BaseTool 和 CrewStructuredTool 的关系是什么提示想想用户接口 vs 内部接口CrewStructuredTool继承自 BaseTool是BaseTool的子类BaseTool对外用户接口是框架暴露给开发者使用的顶层基类定义了所有工具的通用规范继承 ABCPydantic、强制实现_run方法面向普通开发者写自定义工具。CrewStructuredTool框架内部接口是 CrewAI 底层封装的内部实现类。它在 BaseTool 基础上做了结构化参数解析、格式适配、LLM 调用兼容专门给框架内部调度使用承接 BaseTool 再做一层能力增强。简单总结BaseTool 给用户用的工具父类CrewStructuredTool 框架内部基于 BaseTool 实现的结构化工具实现类。Q2invoke()和 Day 3 学的run()有什么区别提示输入方式不同——字典 vs 关键字参数_run()是开发者必须实现的核心业务方法ABC 抽象方法强制要求。入参形式普通位置参数 / 关键字参数写法贴合 Python 常规函数面向开发者写业务逻辑。作用真正执行工具的核心功能invoke()是框架对外的统一调用入口定义在CrewStructuredTool中。入参形式支持字符串、字典两类统一输入input: str | dict还附带config、*kwargs扩展参数。执行逻辑先解析输入参数再内部调用_run()相当于一层包装转发。核心区别invoke()统一入口、接收结构化 / 原始输入框架 / LLM 调用它_run()业务逻辑本体开发者实现它只接收常规函数参数。Q3_parse_args()为什么需要处理字符串和字典两种输入提示LLM 返回的工具参数是什么格式大语言模型LLM输出内容不固定存在两种常见格式字符串格式LLM 直接返回纯文本、拼接后的参数字符串字典 / JSON 格式LLM 按结构化要求返回键值对形式的标准参数主流结构化输出。_parse_args()是参数解析工具作用就是统一收口不管 LLM 吐出来的是字符串还是字典都解析转换成_run()能识别的标准参数格式保证工具正常执行。一句话适配 LLM 不稳定的输出格式做参数统一转换。Q4用户需要直接使用 CrewStructuredTool 吗提示框架自动帮你转换普通开发者只需要继承 BaseTool写自定义工具即可不用感知CrewStructuredTool。当你创建工具、交给 CrewAI 框架调度时框架底层会自动把你写的 BaseTool 实例转换成 CrewStructuredTool 对象完成内部适配。CrewStructuredTool属于内部底层类不对外暴露给终端用户使用。Q5为什么需要两个工具类BaseTool 和 CrewStructuredTool而不是一个提示想想用户友好和内部标准化的矛盾BaseTool → 面向用户追求「简单友好」只保留核心规则强制_run、Pydantic 基础校验API 简洁、上手门槛低让开发者专注写业务逻辑不用关心 LLM 参数解析、内部调度细节。CrewStructuredTool → 面向框架内部追求「标准化、通用性」专门封装复杂逻辑多格式参数解析_parse_args、统一调用入口invoke、LLM 适配、配置转发等。这套复杂逻辑单独放在子类里不会污染对外接口。如果合并成一个类对外接口会变得臃肿复杂新手使用难度大幅提升用户代码和框架底层逻辑耦合在一起后期维护、功能迭代容易出问题。总结BaseTool 做对外简洁接口CrewStructuredTool 做内部复杂标准化实现各司其职。