组件即工具:将可视化节点暴露为 Agent 可调用的 Tool
学习目标
- 理解 Component-as-Tool 的工作原理:如何将画布上的节点变为 Agent 工具
- 掌握
tool_mode=True的使用方法和参数推导规则 - 了解工具执行时的隔离策略(deepcopy + send_message no-op)
- 学会在 Agent 流程中调用组件工具
前置知识
本章涉及工具调用协议的通用原理,建议先阅读:
下文假设你已理解工具调用的基本协议,直接聚焦 Langflow 如何将组件包装为工具的具体实现。
项目实践
1. 从 Input 到 StructuredTool
在 Langflow 中,组件工具化的核心入口是 Input 的 tool_mode 标记:
class VectorStoreComponent: inputs = [ Input( name="query", field_type="str", tool_mode=True, # 标记为 Agent 工具参数 display_name="查询文本", ), Input( name="top_k", field_type="int", default=5, advanced=True, # 不暴露为工具参数 ), ]当组件被注册为工具时,ComponentToolkit 执行以下推导:
参数推导规则:
| Input 属性 | 工具 Schema 映射 |
|---|---|
name | 工具参数名 |
field_type | JSON Schema type(str→string, int→integer, float→number, bool→boolean) |
input_types | 联合类型(如 ["Message", "str"] → string) |
default | JSON Schema default 值 |
description / display_name | 工具参数 description |
advanced=True | 不纳入工具参数 |
2. 执行隔离:防止并发污染
当 Agent 并发调用同一组件工具时(如同时向向量数据库查询多个问题),Langflow 采用深拷贝隔离策略:
def _build_output_function(component, output_method): def output_function(*args, **kwargs): # 深拷贝组件实例,每个调用操作独立的副本 comp = deepcopy(component) local_method = getattr(comp, output_method.__name__) return local_method(*args, **kwargs) return output_function为什么需要深拷贝:组件实例在执行过程中会修改内部状态(如 _results、_token_usage),如果多个并发调用共享同一实例,会导致状态交叉和不可预测的结果。
3. 消息静默:工具执行期间禁止 UI 输出
组件正常执行时会通过 send_message 向 UI 推送中间状态。但作为工具被调用时,这些中间消息不应该出现在用户的对话流中:
def _patch_send_message_decorator(component, func): async def async_wrapper(*args, **kwargs): original_send_message = component.send_message component.send_message = send_message_noop # 替换为 no-op try: return await func(*args, **kwargs) finally: component.send_message = original_send_message # 恢复 return async_wrapper关键设计:仅调用工具的组件自身可以发送消息到 UI,被调用的工具子组件的消息被静默。这样用户只看到最终结果,而非工具内部的执行细节。
问题与规避
| 陷阱 | 现象 | 对策 |
|---|---|---|
| 并发状态污染 | 同时调用同一工具时返回混乱的结果 | Langflow 已自动 deepcopy,但组件内不应使用类级别的共享状态(如 class 级别的 list 或 dict) |
| 工具参数过多 | Agent 收到过多参数,选择困难 | 使用 advanced=True 隐藏非关键参数,仅暴露 tool_mode=True 的核心参数 |
| 工具描述缺失 | Agent 不知道何时调用该工具 | 编写清晰的 description,这是 Agent 决定是否调用工具的唯一依据 |
| 输出类型不匹配 | 下游组件无法接收工具输出 | 确保 Output(types=[...]) 声明的类型与下游组件的 input_types 兼容 |
设计取舍
Component-as-Tool vs 手写 Tool 函数:
| 维度 | Component-as-Tool | 手写 Tool 函数 |
|---|---|---|
| 开发成本 | 低,在现有组件上加标记即可 | 高,需要单独实现工具函数 |
| 一致性 | 工具行为与画布行为一致 | 可能 diverge |
| 灵活性 | 受限于组件的 Input/Output 模型 | 完全自由 |
| 可调试性 | 可在画布中单独测试组件 | 需要独立测试环境 |
Langflow 选择 Component-as-Tool 模式的核心原因是其十元原则之一:「每个后端功能必须落在画布上」。如果一个能力不能作为组件出现在画布上,那它就不应该存在于后端。
参考来源
- 源码:
src/lfx/src/lfx/base/tools/component_tool.py - 源码:
src/lfx/src/lfx/custom/custom_component/component.py - 组件开发指南:
docs/agents/COMPONENTS.md