工具就是 agent 可以执行的动作。运行时,Engine 会把 Decision 里的工具调用分发给 ToolRegistry,由它查找并执行对应 callable。
使用 @tool 把任意 callable 标记成 QitOS 工具。这个装饰器只会附加元数据,不会改变函数行为,所以在测试里依旧可以像普通函数一样直接调用。
from qitos import tool
from qitos.core.tool import ToolPermission
@tool(
name="read_file",
description="Read the contents of a file at the given path.",
timeout_s=10.0,
permissions=ToolPermission(filesystem_read=True),
)
def read_file(path: str) -> str:
with open(path, "r") as f:
return f.read()
| 参数 | 类型 | 说明 |
|---|
name | str | None | 在 Decision.actions 中使用的工具名。默认是函数 __name__。 |
description | str | None | 展示给 LLM 的工具说明。默认回退到 docstring。 |
timeout_s | float | None | 单次调用超时时间,单位秒。None 表示不限时。 |
max_retries | int | 失败时重试次数。默认 0。 |
permissions | ToolPermission | None | 声明该工具需要的系统能力。 |
required_ops | list[str] | None | 环境层面的低级操作能力标识。 |
ToolPermission 描述工具允许做什么。Engine 可以在 preflight 阶段利用这些信息检查环境能力。
from qitos.core.tool import ToolPermission
ToolPermission(
filesystem_read=True,
filesystem_write=False,
network=True,
command=False,
)
四个字段默认都是 False。
ToolRegistry 是 Engine 分发 action 时查询的集合。通常在 AgentModule 构造函数里创建并传入。
from qitos import ToolRegistry
registry = ToolRegistry()
注册单个工具
用 registry.register() 注册一个 callable 或 BaseTool 实例:
@tool(name="add")
def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
registry.register(add)
也可以在注册时覆写名称或元数据:
from qitos.core.tool import ToolMeta
registry.register(add, name="math.add")
registry.register(some_func, meta=ToolMeta(description="Custom description"))
同一个 registry 内工具名必须唯一。重复注册同名工具会抛出 ValueError。
用 include 扫描模块或对象
registry.include(obj) 会扫描 obj 上所有公开且可调用的属性,并注册其中带有 @tool 元数据的成员:
class MyTools:
@tool(name="summarize")
def summarize(self, text: str) -> str:
...
@tool(name="translate")
def translate(self, text: str, lang: str) -> str:
...
tools = MyTools()
registry.include(tools)
当你把一组相关工具封装成类方法时,这通常是最方便的注册方式。
Toolset 指的是任何拥有 tools() 方法、并返回 callable 或 BaseTool 列表的对象。使用 register_toolset() 注册它:
from qitos.kit import CodingToolSet
registry.register_toolset(
CodingToolSet(
workspace_root="/tmp/work",
include_notebook=False,
enable_lsp=False,
enable_tasks=False,
enable_web=False,
expose_modern_names=False,
)
)
来自 toolset 的工具会自动带 namespace:toolset_name.tool_name。你也可以显式指定 namespace:
registry.register_toolset(CodingToolSet(workspace_root="/tmp/work"), namespace="coding")
如果工具需要维护共享状态或生命周期逻辑,可以直接继承 BaseTool:
from qitos.core.tool import BaseTool, ToolSpec, ToolPermission
class DatabaseTool(BaseTool):
"""Query a SQLite database."""
def __init__(self, db_path: str):
self.db_path = db_path
super().__init__(
ToolSpec(
name="query_db",
description="Run a SQL query and return rows as a list.",
parameters={"sql": {"type": "string", "description": "SQL statement"}},
required=["sql"],
permissions=ToolPermission(filesystem_read=True),
)
)
def run(self, sql: str) -> list:
import sqlite3
with sqlite3.connect(self.db_path) as conn:
return conn.execute(sql).fetchall()
registry.register(DatabaseTool(db_path="data.db"))
当你向 register() 传入普通 callable 时,QitOS 内部会自动把它包装成 FunctionTool。大多数情况下你无需显式实例化它。
对于实际编码 agent,通常优先使用 CodingToolSet 或 qitos.kit.toolset 中的 preset builders,而不是手工把文件、shell、web 工具逐个注册。
把 registry 传给 AgentModule
把准备好的 ToolRegistry 传入 AgentModule.__init__():
from qitos import AgentModule, ToolRegistry, tool
class SearchAgent(AgentModule[MyState, dict, Action]):
def __init__(self):
registry = ToolRegistry()
registry.register(web_search)
registry.register(read_file)
super().__init__(tool_registry=registry)
Engine 会读取 agent.tool_registry 并据此创建 ActionExecutor。你也可以调用 registry.get_tool_descriptions() 得到格式化工具说明,用于插入 system prompt:
def build_system_prompt(self, state: MyState) -> str | None:
tools_text = self.tool_registry.get_tool_descriptions()
return f"You have access to these tools:\n\n{tools_text}"
完整示例
from qitos import tool, ToolPermission
@tool(
name="read_file",
timeout_s=5.0,
permissions=ToolPermission(filesystem_read=True),
)
def read_file(path: str) -> str:
"""Read the contents of a file."""
with open(path) as f:
return f.read()
@tool(
name="write_file",
timeout_s=5.0,
permissions=ToolPermission(filesystem_write=True),
)
def write_file(path: str, content: str) -> str:
"""Write content to a file."""
with open(path, "w") as f:
f.write(content)
return f"Written {len(content)} bytes to {path}"