在上一章中,我们详细探讨了 AI-6 框架的后端架构,重点关注引擎如何编排智能体循环(agentic loop)、管理记忆与会话状态(session state),以及如何与多个 LLM 提供商和工具进行集成。我们看到工具与模型是如何被动态发现的,以及引擎如何通过统一抽象来协调它们。这一基础使得工作流具备灵活性与“提供商无关”(provider-agnostic)的特性:支持持久化记忆、可恢复会话,以及安全、可扩展的工具使用。
在本章中,我们将把关注点从整体引擎架构转向对工具系统本身的更深入研究。第 4 章将工具基本视为可插拔的黑盒,而这里我们将“拆开来看”,考察通用工具(generic tools)如何被设计成可复用、一致,并能跨 LLM 提供商可移植。我们将探讨如何用结构化 schema 定义工具、如何进行参数校验,以及工具如何被注册并翻译成各提供商特定的格式。
我们会先讨论 AI-6 如何允许用户按照通用的 AI-6 工具格式来实现一个工具。这个单一的通用工具实现可以与任何 LLM 提供商一起工作,例如 OpenAI、Ollama,甚至未来某个尚不存在的 LLM 提供商。我们还将查看 OpenAI 与 Ollama 的具体 API,理解它们如何通过略有差异的规范提供相似的功能,以及 AI-6 如何在不向用户暴露内部细节的情况下同时利用这两类提供商;随后我们会回顾 schema 定义与输入校验策略。接着,我们将看 AI-6 如何把内部的工具表示映射到提供商特定格式(例如 OpenAI 的 function calling)。最后,我们将考察工具执行是如何被管理的:包括如何捕获结果,并通过会话把结果路由回 LLM。本章将为构建强大、安全、可互操作的工具打下基础,从而扩展智能体 AI 系统的能力。
本章将涵盖以下主要主题:
- 定义通用工具(generic tools)
- 定义 AI-6 自定义工具
- 手动实现一个新工具
- 技术要求
技术要求
本章没有特定的技术要求。如果你希望构建自己的自定义工具,可以使用任何 Python 库。你只需要实现 AI-6 框架本身提供的 Tool 接口,即可定制你的工具。本章代码在此处提供:
github.com/PacktPublis… 。
定义“提供商无关”的工具
每个 LLM 提供商 API(如 OpenAI、Anthropic、Gemini、Ollama)都有各自的工具定义方式。由于 AI-6 需要与所有 LLM 提供商协同工作,它定义了自己的“提供商无关”格式。AI-6 中直接与第三方 LLM 提供商 API 或 SDK 交互的 LLM provider 组件,知道如何把通用工具定义翻译为提供商特定的工具定义。
AI-6 的工具定义必须是所有 API 所需信息的“超集”(superset)。工具定义的重点是函数调用(function calling),同时也支持内置工具(built-in tools)。下面我们将查看 OpenAI 与 Ollama 等 LLM 提供商的 Python SDK 中,工具定义是什么样的。之所以看 Python SDK 而不是原始 REST API,是因为 AI-6 的 LLM provider 是用 Python 实现的,并使用相应第三方提供商的 Python SDK。
我们先看 OpenAI Python SDK,了解它如何指定工具与工具调用。
OpenAI Python 工具规范
OpenAI Python SDK 地址: github.com/openai/open… 。
它由 OpenAI REST API 的 OpenAPI 规范( github.com/openai/open… )自动生成,因此与实际 OpenAI API 100% 一致。
我们关心的工具定义位于 Chat Completions 端点,可通过 Chat Completions API 的 Completions 类的 create() 方法访问( github.com/openai/open… )。不要把它与旧版 Completions API( platform.openai.com/docs/api-re… )混淆。
create() 方法会调用 LLM(可选带很多参数)并返回响应。该响应可以是最终回答,也可以是一次工具调用;工具调用随后会被 AI-6 处理(如上一章所述)。
create() 方法有 8 个重载版本(overloaded versions)。也就是说,有 8 个不同的方法都叫 create(),但参数略有不同。重载的价值在于:对于参数非常多的方法,不同重载版本可以在“更易用”与“更可控”之间提供不同层级的选择:
@overload
def create(
self,
*,
messages: Iterable[ChatCompletionMessageParam],
model: Union[str, ChatModel],
audio: Optional[ChatCompletionAudioParam] | NotGiven = NOT_GIVEN,
frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN,
function_call: completion_create_params.FunctionCall | NotGiven = NOT_GIVEN,
functions: Iterable[completion_create_params.Function] | NotGiven = NOT_GIVEN,
logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN,
logprobs: Optional[bool] | NotGiven = NOT_GIVEN,
max_completion_tokens: Optional[int] | NotGiven = NOT_GIVEN,
max_tokens: Optional[int] | NotGiven = NOT_GIVEN,
metadata: Optional[Metadata] | NotGiven = NOT_GIVEN,
modalities: Optional[List[Literal["text", "audio"]]] | NotGiven = NOT_GIVEN,
n: Optional[int] | NotGiven = NOT_GIVEN,
parallel_tool_calls: bool | NotGiven = NOT_GIVEN,
prediction: Optional[ChatCompletionPredictionContentParam] | NotGiven = NOT_GIVEN,
presence_penalty: Optional[float] | NotGiven = NOT_GIVEN,
reasoning_effort: Optional[ReasoningEffort] | NotGiven = NOT_GIVEN,
response_format: completion_create_params.ResponseFormat | NotGiven = NOT_GIVEN,
seed: Optional[int] | NotGiven = NOT_GIVEN,
service_tier: Optional[Literal["auto", "default", "flex", "scale"]] | NotGiven = NOT_GIVEN,
stop: Union[Optional[str], List[str], None] | NotGiven = NOT_GIVEN,
store: Optional[bool] | NotGiven = NOT_GIVEN,
stream: Optional[Literal[False]] | NotGiven = NOT_GIVEN,
stream_options: Optional[ChatCompletionStreamOptionsParam] | NotGiven = NOT_GIVEN,
temperature: Optional[float] | NotGiven = NOT_GIVEN,
tool_choice: ChatCompletionToolChoiceOptionParam | NotGiven = NOT_GIVEN,
tools: Iterable[ChatCompletionToolParam] | NotGiven = NOT_GIVEN, # 👈
top_logprobs: Optional[int] | NotGiven = NOT_GIVEN,
top_p: Optional[float] | NotGiven = NOT_GIVEN,
user: str | NotGiven = NOT_GIVEN,
web_search_options: completion_create_params.WebSearchOptions | NotGiven = NOT_GIVEN,
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
) -> ChatCompletion:
"""
...
"""
部分重载方法是同步的,部分是异步的,但它们定义 tools 的方式相同:
tools: Iterable[ChatCompletionToolParam] | NotGiven = NOT_GIVEN,
这行是什么意思?
Iterable[ChatCompletionToolParam]表示:传入的 tools 应该是一个可迭代序列,其中每个元素的类型是ChatCompletionToolParam。| NotGiven = NOT_GIVEN表示:tools 参数也可以是NotGiven类型;如果调用者不传 tools,则使用默认值NOT_GIVEN(它是NotGiven类型的常量)。
换句话说,tools 参数可以是两种类型之一(Python 允许这种联合类型)。第一种是 Iterable[ChatCompletionToolParam](你可以把它理解成由 ChatCompletionToolParam 构成的 list)。第二种是 NotGiven。tools 的默认值是 NOT_GIVEN:如果调用者没有提供任何 tools,那么 tools 的值就是默认的 NOT_GIVEN。
如果你在想 NotGiven 是什么:它是一个简单的哨兵(sentinel)类,用来表示“参数未指定”,从而让参数取默认值 NOT_GIVEN。
好,我们再看 ChatCompletionToolParam。它是一个包含两个字段的类:function 与 type,两者都是必填。function 字段类型为 FunctionDefinition,而 type 字段必须是字面量字符串 "function"。
定义如下:
class ChatCompletionToolParam(TypedDict, total=False):
function: Required[FunctionDefinition]
type: Required[Literal["function"]]
"""The type of the tool. Currently, only `function` is supported."""
这里容易让人困惑,因为出现了两类“type”:
Required[FunctionDefinition]是 Python 类型提示,表示必填字段的 Python 类型是FunctionDefinition。- 而
type: Required[Literal["function"]]里的"function"则是 OpenAI 工具类型字段的字面量值。
OpenAI 的 Chat Completions API 有趣的一点是:它实际上只支持一种工具类型——函数工具(function tool),但 API 定义得更通用,好像未来可以支持更多工具类型一样。
OpenAI 的其他 API 的确支持多种工具类型。已废弃的 Assistants API(beta)( github.com/openai/open… )支持两种内置工具:file_search 与 code_interpreter。
OpenAI 的 API 设计迭代确实经历了一段“长途旅行”:最初只有 Chat Completion API(我们正在用的、且只支持 "function" 工具类型);之后出现了长期处于 beta、最终被弃用的 Assistants API;再后来发布了 Responses API,引入更多工具类型。
下面是内置 CodeInterpreter 与 FileSearch 工具的定义:
class ToolResourcesCodeInterpreter(BaseModel):
file_ids: Optional[List[str]] = None
"""
A list of [file](https://platform.openai.com/docs/api-reference/files) IDs made
available to the `code_interpreter`` tool. There can be a maximum of 20 files
associated with the tool.
"""
class ToolResourcesFileSearch(BaseModel):
vector_store_ids: Optional[List[str]] = None
"""
The ID of the
[vector store](https://platform.openai.com/docs/api-reference/vector-stores/object)
attached to this assistant. There can be a maximum of 1 vector store attached to
the assistant.
"""
class ToolResources(BaseModel):
code_interpreter: Optional[ToolResourcesCodeInterpreter] = None
file_search: Optional[ToolResourcesFileSearch] = None
新的 Responses API 在 types/responses/tool_param.py( github.com/openai/open… )中支持以下工具:
ToolParam: TypeAlias = Union[
FunctionToolParam,
FileSearchToolParam,
WebSearchToolParam,
ComputerToolParam,
Mcp,
CodeInterpreter,
ImageGeneration,
LocalShell,
]
目前,AI-6 的 OpenAI LLM provider 使用的是 Chat Completions API,因此我们回到 FunctionDefinition 类( github.com/openai/open… )。它代表一个函数工具,包含 name、description、可选 parameters,以及可选 strict 标志(默认为 False,除非显式设为 True):
class FunctionDefinition(BaseModel):
name: str
"""The name of the function to be called.
Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length
of 64.
"""
description: Optional[str] = None
"""
A description of what the function does, used by the model to choose when and
how to call the function.
"""
parameters: Optional[FunctionParameters] = None
"""The parameters the functions accepts, described as a JSON Schema object.
See the [guide](https://platform.openai.com/docs/guides/function-calling) for
examples, and the
[JSON Schema reference](https://json-schema.org/understanding-json-schema/) for
documentation about the format.
Omitting `parameters` defines a function with an empty parameter list.
"""
strict: Optional[bool] = None
"""Whether to enable strict schema adherence when generating the function call.
If set to true, the model will follow the exact schema defined in the
`parameters` field. Only a subset of JSON Schema is supported when `strict` is
`true`. Learn more about Structured Outputs in the
[function calling guide](docs/guides/function-calling).
"""
继续深挖 FunctionDefinition 里的 FunctionParameters( github.com/openai/open… )。它其实就是一个简单的 string -> any object 字典:
# File generated from our OpenAPI spec by Stainless.
from typing import Dict
from typing_extensions import TypeAlias
__all__ = ["FunctionParameters"]
FunctionParameters: TypeAlias = Dict[str, object]
我们总结一下 OpenAI Chat Completions API 的工具定义要求(按 OpenAI 的规范):
- 每个工具都必须是
"function"类型 - 每个工具都有 name 与 description
- 工具可以有 0 个或多个 parameters(每个参数有名字与任意值类型)
- 工具可以指定 strict(是否严格约束输出)
OpenAI 工具规范还有一个重要元素:除了 create() 的 tools 参数,还有 tool_choice 参数:
tool_choice: ChatCompletionToolChoiceOptionParam | NotGiven = NOT_GIVEN,
tool_choice 可以取以下值之一:
"none"(不调用任何工具)"auto"(模型自行决定是否调用工具、以及调用哪些工具)"required"(必须至少调用一个工具,但由模型决定具体调用哪些)- 或指定某个具体工具名(必须调用该工具)
其类型定义如下:
ChatCompletionToolChoiceOptionParam: TypeAlias = Union[
Literal["none", "auto", "required"],
ChatCompletionNamedToolChoiceParam
]
如果你觉得这听起来很绕,那是因为它确实绕。作者还专门写过一篇博客讨论 tool_choice 的问题,并说明如何大幅简化:Fixing the OpenAI Tool Calling API( medium.com/@the.gigi/f… )。
好,记住这些,我们再看 Ollama Python SDK 如何指定工具调用。
Ollama Python 工具规范
Ollama Python SDK: github.com/ollama/olla… 。
它本身已经是对多个 API 的抽象,因为它通过统一接口支持多个提供商。我们看看它如何在 Ollama Python SDK 里指定工具。
Ollama 通过 Client 类的 chat() 方法(多个重载,类似 OpenAI)与 LLM 交互,并在其中指定 tools( github.com/ollama/olla… ):
@overload
def chat(
self,
model: str = '',
messages: Optional[
Sequence[Union[Mapping[str, Any], Message]]] = None,
*,
tools: Optional[
Sequence[Union[Mapping[str, Any], Tool, Callable]]] = None, # 👈
stream: Literal[False] = False,
think: Optional[bool] = None,
format: Optional[Union[Literal['', 'json'], JsonSchemaValue]] = None,
options: Optional[Union[Mapping[str, Any], Options]] = None,
keep_alive: Optional[Union[float, str]] = None,
) -> ChatResponse: ...
tools 的定义很“长”。我们拆解一下:
tools 参数是可选的,默认值为 None。若提供,它必须是一个序列(例如 list 或 tuple),并且序列中的每个元素必须是以下三种类型之一:
Mapping[str, Any]:以字典形式表达的 JSON schema(string 键、任意类型值)Tool:Ollama SDK 提供的 Tool 实例(封装工具元数据与行为)Callable:遵循 Google 风格 docstring 的可调用对象,可被转换成 Ollama Tool 实例
完整类型如下:
tools: Optional[
Sequence[Union[Mapping[str, Any], Tool, Callable]]
] = None
无论最初采用何种表示,它们最终都会变成一组 Ollama Tool 实例( github.com/ollama/olla… ):
class Tool(SubscriptableBaseModel):
type: Optional[Literal['function']] = 'function'
class Function(SubscriptableBaseModel):
name: Optional[str] = None
description: Optional[str] = None
class Parameters(SubscriptableBaseModel):
model_config = ConfigDict(populate_by_name=True)
type: Optional[Literal['object']] = 'object'
defs: Optional[Any] = Field(None, alias='$defs')
items: Optional[Any] = None
required: Optional[Sequence[str]] = None
class Property(SubscriptableBaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
type: Optional[Union[str, Sequence[str]]] = None
items: Optional[Any] = None
description: Optional[str] = None
enum: Optional[Sequence[Any]] = None
properties: Optional[Mapping[str, Property]] = None
parameters: Optional[Parameters] = None
function: Optional[Function] = None
我们不会逐行解析这段代码,只需要注意:Ollama 在工具定义上附加了一些模型相关信息,这些信息大概率用于将 Tool 翻译为具体模型的工具定义。
关键点是:与 OpenAI Chat Completions API 类似,Ollama 也只支持 "function" 这一种工具类型。
总结一下:OpenAI 与 Ollama 各自用自己的格式定义工具,但从概念上看,它们都能表达每个工具的等价信息,这对 AI-6 来说是个好消息。
现在我们就可以来看 AI-6 的通用工具规范 schema 了。
定义 AI-6 工具规范 schema
我们在上一章已经介绍过 AI-6 的 Parameter 与 Tool 类。这里做一个快速回顾。AI-6 的目标是保持简单,不搞太多花活。AI-6 有一个 OpenAI 与 Ollama 的工具规范中缺少的特殊特性:Tool 类的 required 字段。这个字段是一组参数名,表示 LLM 调用工具时必须提供这些参数。它在 OpenAI 的 REST API 工具规范里是存在的,但不知为何没有出现在 Python SDK 里:
class Parameter(NamedTuple):
name: str
type: str
description: str
@dataclass(slots=True)
class Tool(ABC):
name: str
description: str
parameters: list[Parameter]
required: set[str]
def configure(self, config: dict) -> None:
"""Optional: configure the tool with given parameters."""
@abstractmethod
def run(self, **kwargs) -> str:
"""Execute the tool using the given arguments and return the result as a string."""
这段直观的代码就是 AI-6 的完整工具规范 schema:非常简单,也很容易理解。
你已经看过 AI-6 如何通过 LLMProvider 接口抽象不同提供商及其实现,从而定义工具。接下来我们将在下一节看到:如何定义可以被翻译到当前激活的 LLM 提供商工具格式的自定义工具。
定义 AI-6 自定义工具
AI-6 自带了许多开箱即用的工具示例,实现新工具也很容易。本节我们将先回顾一些现有工具,然后从零实现一个全新的工具。
定义一个 AI-6 自定义工具的流程包括以下步骤:
1)创建一个新的 Python 类。
2)直接或间接继承 Tool 基类。
3)实现 run() 方法。
4)可选实现 configure() 方法。
我们先从通用命令行工具开始——它们非常强大。
通用命令行工具
AI-6 开箱即用提供的很多实用工具都是命令行工具。这些工具会在 AI-6 运行的任何地方执行(你的机器、容器、云端等)。AI-6 为命令行工具提供了一个基类,处理了运行带任意参数的命令行程序所需的全部样板代码(boilerplate),并提供了一个选项:可以以“专用用户”(不同于当前用户)的身份运行命令行程序。我们先看看 CommandTool 类是怎么工作的,再去看基于它派生的工具。
LLM 并不知道自己在调用命令行工具。你还记得:LLM 接收的是工具定义,然后通过 agentic loop 发起“调用这些工具”的请求。真正的工具执行发生在 AI 系统/框架这一侧,而很多时候,这里执行的就是命令行工具。
CommandTool 会引入 sh 包(用于运行 shell 命令,sh.readthedocs.io/en/latest/i… )、内置的 shlex 包(用于解析 POSIX 命令行参数,docs.python.org/3/library/s… ),以及 AI-6 对象模型里的 Parameter 和 Tool:
import sh
import shlex
from backend.object_model import Tool, Parameter
CommandTool 类的组织方式如下,用来帮助你实现命令行工具:
- 该类继承自 Tool 基类,因此它本身就能作为 AI-6 工具的基类。
- 构造函数输入包括:命令名(path 上可用的命令行程序名)、可选的 user、以及命令文档链接(可选)。
- 它会构造一个 parameters 列表,其中只包含一个字符串参数 args,表示传给目标命令的“全部命令行参数”。
- args 参数是必填的,但它可以是空字符串。如果提供了文档链接,它会被加入到工具描述里,从而帮助 LLM 理解如何调用该命令。
类定义如下:
class CommandTool(Tool):
def __init__(
self, command_name: str, user: str | None = None,
doc_link: str = ""
):
self.command_name = command_name
self.user = user
description = f'{command_name} tool. ' + f'See {doc_link}' if doc_link else ''
parameters = [Parameter(name='args', type='string',
description=f'command-line arguments for {command_name}')]
required = {'args'}
super().__init__(
name=command_name,
description=description,
parameters=parameters,
required=required
)
run() 方法有两个工作:
1)用 shlex.split() 解析 args 参数里的命令行参数;
2)以专用用户(若提供)或当前用户执行该命令。注意:如果要切换到专用用户,当前用户必须能通过 sudo 切换过去:
def run(self, **kwargs):
args = shlex.split(kwargs['args'])
if self.user is not None:
return sh.sudo('-u', self.user, self.command_name, *args)
else:
return getattr(sh, self.command_name)(*args)
CommandTool 类没有提供特殊的 configure() 方法来覆盖可选的 Tool.configure(),但如果需要,从 CommandTool 派生的工具类可以自行实现。
现在我们来看看:实现命令行工具有多么简单,以及它如何为 AI agent 提供很强的能力。我们先从文件系统工具开始。
文件系统工具
一些最有用的命令都围绕文件与目录展开。AI-6 提供了基础的目录/文件命令,比如 pwd、ls、cat、echo。
它还提供了更“进阶”的文件编辑命令,例如 sed、awk、patch。我们从 pwd(print working directory)开始。我们要做的只是定义一个名为 Pwd 的类,并在一个最小构造函数里提供 POSIX pwd 命令的文档链接:
from backend.tools.base.command_tool import CommandTool
class Pwd(CommandTool):
def __init__(self, user: str | None = None):
doc_link = 'https://www.gnu.org/software/coreutils/manual/html_node/pwd-invocation.html'
super().__init__('pwd', user=user, doc_link=doc_link)
我们甚至可以不依赖 AI-6 其他部分,直接测试它:实例化后检查 parameters、description 等属性:
❯ source venv/bin/activate
❯ python
Python 3.13.3 (main, Apr 8 2025, 13:54:08) [Clang 17.0.0 (clang-1700.0.13.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from backend.tools.file_system.pwd import Pwd
>>> pwd = Pwd()
>>> pwd.parameters
[Parameter(name='args', type='string', description='command-line arguments for pwd')]
>>> pwd.description
'pwd tool. See https://www.gnu.org/software/coreutils/manual/html_node/pwd-invocation.html'
也可以运行它,验证它确实返回当前工作目录:
>>> pwd.run(args='')
'/Users/gigi/git/ai-six/py\n'
其他文件系统命令同样简单。下面是 ls 命令:
from ..base.command_tool import CommandTool
class Ls(CommandTool):
def __init__(self, user: str | None = None):
super().__init__(command_name='ls', user=user,
doc_link = 'https://www.gnu.org/software/coreutils/manual/html_node/ls-invocation.html'
)
下面是 awk 命令:
from ..base.command_tool import CommandTool
class Awk(CommandTool):
def __init__(self, user: str | None = None):
super().__init__(command_name='awk', user=user,
doc_link='https://www.gnu.org/software/gawk/manual/gawk.html')
大多数其他文件系统工具也遵循同样的模式,但有一个例外:echo 工具。我们下一节专门讨论它。
实现 echo 工具
echo 工具不一样。echo 的目的,是让 LLM 能创建新文件或替换已有文件。这当然是非常有用的能力。但要在 shell 里做到这一点,通常需要借助重定向,例如:
> echo "123" > ~/ttt/some-file
> cat ~/ttt/some-file
123
请记得管理工具的安全与权限(正如我们在第 4 章讨论过的)。
我们的 CommandTool 基类并没有提供把命令输出重定向到文件的能力。一种做法是扩展 CommandTool 来支持输出重定向;但也可以直接实现所需功能。我们来看看它怎么做。
导入部分类似,但这里会引入内置的 os 包和 Tool 模块,而且不会导入 shlex 或 CommandTool——原因很快就会看到:
import os
import sh
from backend.object_model import Tool, Parameter
Echo 类直接继承 Tool,而不是 CommandTool,因为它需要自己实现 run():
class Echo(Tool):
构造函数更“具体”一些。因为它不去调用系统内置 echo 命令,所以 description 不包含外部文档链接。它还有两个必填参数:content 和 file_path,而不是 CommandTool 派生工具统一的 args 参数。同时它也保存 user(默认 None):
def __init__(self, user: str | None = None):
self.user = user
desc = 'Write content to a file, creating any necessary directories.'
super().__init__(
name='echo',
description=desc,
parameters=[
Parameter(
name='file_path', type='string',
description='The path of the file to write to.'),
Parameter(
name='content', type='string',
description='The content to write to the file.')
],
required={'file_path', 'content'}
)
run() 方法会从参数里取出要写入的文件名与内容(记住:这些参数在 LLM 为创建/替换文件而调用工具时会提供),然后拿到目标文件所在目录:
def run(self, **kwargs):
filename = kwargs['file_path']
content = kwargs['content']
dir_path = os.path.dirname(filename)
接着逻辑会根据 user 分支:
- 如果提供了 user,就用 sudo + shell 命令(mkdir、tee)来创建目录并写入/替换文件:
if self.user is not None:
sh.sudo('-u', self.user, 'mkdir', '-p', dir_path)
sh.sudo('-u', self.user, 'tee', filename,
_in=content, _out=os.devnull)
return f"Content written to {filename} as user {self.user}"
- 如果没有 user,则直接用 Python 创建目录并写文件:
os.makedirs(dir_path, exist_ok=True)
with open(filename, 'w') as file:
file.write(content)
return f"Content written to {filename}"
现在我们将把注意力转向另一个实用工具:测试运行器(test runner)。
测试运行器工具
文件系统工具让 AI-6 能修改它自己的代码!没错,想一想:
- 它可以用 ls 列出自己的文件与目录;
- 用 cat 读取自己的源文件内容;
- 用 echo 创建新文件(或替换已有文件);
- 还可以用 sed、awk、patch 修改已有文件。
但 agentic AI 框架和 LLM 目前还做不到“一次就完全正确”。最佳实践仍然是:迭代式修改代码,并在提交前验证修改确实可用。test_runner 工具就是为此而生。该工具可以在任意目录里运行使用标准 Python unittest 框架编写的单元测试。我们看看它怎么实现。
照例,它导入 sh,以及 AI-6 对象模型中的 Tool 和 Parameter:
import sh
from backend.object_model import Tool, Parameter
TestRunner 类直接继承 Tool。构造函数调用 Tool 基类,传入描述与一个必填参数 test_directory:
class TestRunner(Tool):
def __init__(self):
desc = 'A tool to run Python unit tests using the unittest framework.'
super().__init__(
name='python_test_runner',
description=desc,
parameters=[Parameter(name='test_directory',
type='string',
description='The directory containing tests to run')],
required={'test_directory'}
)
run() 方法会在目标目录运行所有单元测试:它调用 Python 解释器,以 unittest 模块方式执行,并让其 discover 该目录下的测试。它返回一个字典,包含 "stdout" 与 "stderr" 两个 key。对于 sh 的执行异常会“优雅处理”,但其他异常会继续向上抛出交给调用方处理:
def run(self, **kwargs):
test_directory = kwargs['test_directory']
try:
result = sh.python('-m', 'unittest', 'discover', '-s',
test_directory)
return {"stdout": str(result), "stderr": ""}
except sh.ErrorReturnCode as e:
return {"stdout": e.stdout.decode('utf-8'),
"stderr": e.stderr.decode('utf-8')}
看一下它的运行效果。AI-6 后端单元测试在 Python 根目录下的相对路径 backend/tests:
>>> from backend import tests
>>> tests_dir = tests.__path__[0]
>>> tests_dir
'/Users/gigi/git/ai-six/py/backend/tests'
拿到测试目录后,就可以实例化并调用 test runner,验证返回结果成功:
>>> result = tr.run(test_directory=tests_dir)
>>> assert(result['stdout'] != '')
>>> assert(result['stderr'] == '')
很多时候,你需要启动或重启应用才能使用你新增/修改的工具。bootstrap 工具就是为此准备的。我们继续看。
Bootstrap 工具
如我们所见,AI-6 内置工具支持修改自身代码并测试这些修改(因为 AI-6 单元测试使用 unittest 框架)。拼图的最后一块,是在完成修改与测试后,重启控制 AI-6 的程序。bootstrap 工具就是做这个的:当 AI-6 代码被修改后,LLM 可能会决定重启控制系统,从而以修改后的代码重新启动:
import importlib
from backend.object_model import Tool
import os
import sys
Bootstrap 类直接继承 Tool。构造函数很简单,因为它不需要参数。description 说明会使用 execv(linux.die.net/man/3/execv)来重启程序:
class Bootstrap(Tool):
def __init__(self):
desc = 'Tool to restart the program using execv.'
super().__init__(
name='bootstrap',
description=desc,
parameters=[], # No parameters needed for execv
required=set()
)
run() 方法比前面看到的工具更复杂。我们拆解一下:
首先,它定位程序主模块(main)的文件名。如果找不到,就抛异常:
def run(self, **kwargs):
main_module = sys.modules['__main__']
module_path = getattr(main_module, '__file__', None)
if not module_path:
raise RuntimeError("Cannot determine __main__.__file__; are you in a REPL or notebook?")
如果 main module 有 spec 属性,说明它是以 python -m module 的方式运行,需要准备对应参数:
spec = getattr(main_module, '__spec__', None)
if spec and spec.name:
# Executed as a module: python -m package.module
module_name = spec.name
args = [sys.executable, '-m', module_name, *sys.argv[1:]]
否则,它是以 python <main file> 的方式运行,需要准备脚本模式参数:
else:
# Executed as a script: python script.py
args = [sys.executable, module_path, *sys.argv[1:]]
最后,用 os.execv() 携带正确的命令行参数重启程序:
os.execv(sys.executable, args)
bootstrap 是一个强大的工具:它允许 LLM 在修改 AI-6 代码后重启控制系统。它很简单,但能力很强,可以支撑涉及“自修改代码”的复杂工作流。不过我们还能走得更远:让 LLM 直接操控当前对话的记忆(memory)。
记忆工具
如你所知,AI-6 有一个强大的记忆系统,核心是 session 概念:它是一个持久化的消息集合,能跨多个 agentic loop。session 会被存成文件;我们可以允许 LLM 和/或用户直接操作这些 session 文件并加载它们。这意味着 AI-6 允许 LLM 按其认为合适的方式重写当前 session 的记忆。这种能力非常强,必须极其谨慎地使用。通常 LLM 不会主动去直接操作 session 记忆,但用户可能会指示它列出历史 session 并加载某个 session,以便从中断处继续。
还要记住:session 文件会在 session 结束后仍保留在磁盘上,因此使用 AI-6 的 AI 系统必须确保用户权利与隐私得到维护,并提供删除 session 的选项。
我们来看这些记忆工具是如何实现的。共有四个与记忆相关的工具:
- list_sessions:列出记忆目录下的所有 session
- load_session:按 ID 加载一个 session
- get_session_id:获取当前 session 的 ID
- delete_session:按 ID 删除一个 session
注意:没有保存当前 session 的工具,因为 AI-6 会定期自动保存 session(见第 4 章讨论的 checkpoint interval 配置)。
这里采用多个记忆工具,而不是一个包含多个 action 的“单一记忆工具”,是刻意的设计选择:它能显著简化每个工具的实现与测试。
列出 sessions
ListSessions 工具只需要导入 json(用于返回 sessions 的格式)以及 Tool 基类。该工具不需要命令行参数:
import json
from backend.object_model import Tool
class ListSessions(Tool):
"""Tool to list all available sessions in memory."""
构造函数会保存 Engine 实例引用,后续用它来真正列 session。由于该工具不需要参数,parameters 列表为空,required 集合也为空:
def __init__(self, engine=None):
"""Initialize the tool.
Args:
engine: Reference to the Engine instance
"""
self.engine = engine
super().__init__(
name='list_sessions',
description='List all available sessions in memory.',
parameters=[],
required=set()
)
run() 方法负责实际列出 sessions:先检查 engine 引用是否存在;若不存在返回错误信息。若存在,则调用 engine.list_sessions() 获取 session ID 列表。如果没有 session,就返回提示;否则把 session ID 列表序列化为 JSON 并返回格式化字符串:
def run(self, **kwargs):
"""List all available sessions.
Returns:
String with the details of all stored sessions (id, title and filename)
"""
if not self.engine:
return "Error: Engine reference not set."
sessions = self.engine.list_sessions()
if not sessions:
return "No sessions found in memory."
return "Available sessions:\n" + json.dumps(sessions)
把实际工作委托给 Engine 实例的模式,在所有记忆工具中都很常见。Engine 是 AI-6 记忆系统的“事实来源”(source of truth),负责管理 session、消息与其他记忆相关任务。无需在工具中重复这些逻辑,否则只会让代码更复杂、更难维护。
加载 sessions
加载 session 时,需要指定 session ID。工具会让 Engine 把该 session 加载到内存中。实现如下。
只需要从对象模型导入 Tool 和 Parameter:
from backend.object_model import Tool, Parameter
LoadSession 类继承 Tool。构造函数保存 Engine 引用,并要求一个必填参数 session_id:
class LoadSession(Tool):
"""Tool to load a specific session by ID."""
def __init__(self, engine=None):
"""Initialize the tool.
Args:
engine: Reference to the Engine instance
"""
self.engine = engine
super().__init__(
name='load_session',
description='Load a specific session by ID.',
parameters=[
Parameter(
name='session_id',
type='string',
description='ID of the session to load'
)
],
required={'session_id'}
)
run() 方法执行加载:检查 engine 引用与 session_id 是否提供;若不满足则返回错误信息。然后调用 engine.load_session(session_id)。成功则返回成功消息,否则返回失败消息(可能是 session 不存在或加载失败):
def run(self, session_id, **kwargs) -> str:
"""Load a specific session.
Args:
session_id: ID of the session to load
Returns:
String indicating success or failure
"""
if not self.engine:
return "Error: Engine reference not set."
if not session_id:
return "Error: Session ID is required."
success = self.engine.load_session(session_id)
if success:
return f"Successfully loaded session: {session_id}"
else:
return f"Failed to load session: {session_id}. Session may not exist."
有时,获取当前 session ID 也很有用,便于之后加载。下面是实现方式。
获取当前 session ID
获取当前 session ID 是个简单操作:工具只需要返回 Engine 存储的当前 session ID。
只需要导入 Tool,不需要参数:
from backend.object_model import Tool
GetSessionId 类同样遵循熟悉的模式:构造函数接收 engine 引用,parameters 为空,required 也为空:
class GetSessionId(Tool):
"""Tool to get the current session ID."""
def __init__(self, engine=None):
"""Initialize the tool.
Args:
engine: Reference to the Engine instance
"""
self.engine = engine
super().__init__(
name='get_session_id',
description='Get the ID of the current session.',
parameters=[],
required=set()
)
run() 方法进行校验后,把工作委托给 Engine:调用 engine.get_session_id() 并返回格式化字符串:
def run(self, **kwargs):
"""Get the current session ID.
Returns:
String with the current session ID
"""
if not self.engine:
return "Error: Engine reference not set."
session_id = self.engine.get_session_id()
return f"Current session ID: {session_id}"
有时也需要清理旧 session 避免杂乱。下面是删除方式。
删除 sessions
删除 session 是一项强操作,需要谨慎。它会删除持久化的 session 文件,因此必须确认该 session 已不再需要。和前面一致,工具会通过 Engine 来执行实际删除。到这里,你应该已经熟悉 import、类声明和构造函数的写法了:
from backend.object_model import Tool, Parameter
class DeleteSession(Tool): """Tool to delete a specific session by ID."""
def __init__(self, engine=None):
"""Initialize the tool.
Args:
engine: Reference to the Engine instance
"""
self.engine = engine
super().__init__(
name='delete_session',
description='Delete a specific session by ID.',
parameters=[
Parameter(
name='session_id',
type='string',
description='ID of the session to delete'
)
],
required={'session_id'}
)
run() 方法执行删除:检查 engine 引用与 session_id 参数是否提供;并额外增加一个安全检查——禁止删除当前 session。这是为了避免误删活跃 session,从而让引擎处于无效状态:
def run(self, session_id, **kwargs):
"""Delete a specific session.
Args:
session_id: ID of the session to delete
Returns:
String indicating success or failure
"""
if not self.engine:
return "Error: Engine reference not set."
if not session_id:
return "Error: Session ID is required."
# Don't allow deleting the current session
if session_id == self.engine.get_session_id():
return "Error: Cannot delete the current active session."
success = self.engine.delete_session(session_id)
if success:
return f"Successfully deleted session: {session_id}"
else:
return f"Failed to delete session: {session_id}. Session may not exist."
本节我们覆盖了很多内容:基于第 4 章的工具系统设计,我们回顾了 AI-6 中多个自定义工具的实现方式;也看到这些工具如何被用来修改 AI-6 代码、测试修改、重启程序,以及操控 session 记忆。这是一组非常强的工具集合,使得 AI-6 在理论上能够以“良性循环”的方式改进自身。
下一节我们将看到:如何把这些工具真正用起来,让 AI-6 从零实现一个新工具。
用 AI-6 从零实现一个新工具
AI-6 自带了许多内置工具,从理论上讲,这些工具让它能够改进自己的代码。它可以读写自己的源代码、编写新代码与测试、运行测试并重启程序。它还有一个 Git 工具(我们没有深入展开),可以让 AI-6 执行任何 Git 操作,例如克隆仓库、创建分支、添加文件、提交修改并推送到远程仓库。
但 AI-6 没有一个用来与 GitHub 交互的工具。而像 GitHub 这样的集中式源代码管理系统,是现代软件开发的核心。下面我们让 AI-6 “跑一圈”,看看它能否在我们提供一些有用帮助的情况下,为自己构建工具。
如何与 AI-6 交互
AI-6 开箱即用支持多种接口:chatbot CLI、基于 Slack 的接口,以及 Web UI 接口。
在这个小项目里,我们将通过 AI-6 的 chatbot CLI 使用 AI-6。它与我们在第 3 章看到的 k8s-ai 接口非常类似。Slack 与 Web 接口我们会在下一章详细介绍。
当我们使用 chatbot CLI 时,它就扮演了“使用 AI-6 的 AI 系统”。启动 chatbot CLI 的方式如下:
❯ python -m frontend.cli.ai6
Using .env file from /Users/gigi/git/ai-six/py/frontend/cli/.env
接下来是在聊天界面中的交互:
👤 [You]: hi
🤖 [AI-6]: Hello! How can I assist you today?
——————
👤 [You]:
前置步骤就绪后,我们就可以开始与 AI-6 交互,构建一个闪亮的新 GitHub 工具了。
用 AI-6 构建一个新的 GitHub 工具
我们的目标是:给出对工具的高层描述,让 AI-6 基本完成所有工作。这会是一个高度交互的过程:我们提供指导,AI-6 去实现,每一步我们都验证结果并给反馈。
我们要实现的工具是一个 GitHub 工具,它将允许 AI-6 与 GitHub 仓库、Issue、Pull Request 等进行交互。我们从描述要实现的工具开始:
👤 [You]: I want to create a command-line tool for interacting with Github using the gh CLI (already installed and configured) similar to the tools in /Users/gigi/git/ai-six/py/backend/tools. how do you propose to that?
🤖 [AI-6]: To create a command-line tool for interacting with GitHub using the GitHub CLI (gh), you can follow these steps to set up a structure similar to existing tools like the Ollama tool.
Here’s how you can achieve this:
Create a Base Command Tool Class (if not already existing):
This class (like CommandTool) will handle generic CLI command execution, including setting up basic configurations like user data.
Create the GitHub CLI Tool Class:
Use this class to define the GitHub-specific command-line interactions. ```
好,这是个不错的开头,但我们不需要创建 base command tool class——它已经存在。我们把这一点告诉 AI-6。
👤 [You]: Since the Ollama tool exists, then it probably the CommandTool base class already exists
🤖 [AI-6]: You're correct. If the `Ollama` tool utilizes a `CommandTool` base class, then it likely already exists in your project structure. In this case, you can directly proceed to implement the GitHub CLI tool by leveraging this existing base class. Here's how you can do it:
### Step-by-Step Guide to Create the GitHub CLI Tool
1. **Locate the Existing `CommandTool` Base Class:**
- First, confirm the location and available methods of the `CommandTool` class within your project.
2. **Create the GitHub CLI Tool Class:**
With the base class available, you can create a new tool class that extends `CommandTool` for GitHub interactions.
# gh_cli_tool.py
# Import the CommandTool class from its existing location
from backend.tools.base.command_tool import CommandTool
class GhCLI(CommandTool):
def __init__(self, user: str | None = None):
super().__init__(
command_name='gh', user=user,
doc_link='https://cli.github.com/manual/')
def list_repositories(self) -> str:
return self.run_command('repo list')
def create_repository(self, name: str) -> str:
return self.run_command(f'repo create {name}')
def clone_repository(self, repo: str) -> str:
return self.run_command(f'repo clone {repo}')
# Add more methods to interact with other GitHub functionalities using the 'gh' CLI
嗯,AI-6 看起来并没有花时间研究 CommandTool 基类。它建议加一些方法,比如 list_repositories()、create_repository(name: str)、clone_repository(repo: str)。这些方法并不必要:我们只要用 CommandTool 的 run() 方法,就能用任意参数执行任意命令行程序。
我们把这一点再讲清楚,并让 AI-6 先去把 CommandTool 的代码找出来给我们看:
👤 [You]: hold on. let's do this step by step. find and show me the code for the CommandTool base class
🤖 [AI-6]: Here is the code for the `CommandTool` base class:
import sh
import shlex
from backend.object_model import Tool, Parameter
class CommandTool(Tool):
def __init__(
self, command_name: str, user: str | None = None,
doc_link: str = ""
):
self.command_name = command_name
self.user = user
description = f'{command_name} tool. ' + f'See {doc_link}' if doc_link else ''
parameters = [Parameter(
name='args', type='string',
description=f'command-line arguments for {command_name}')]
required = {'args'}
super().__init__(
name=command_name,
description=description,
parameters=parameters,
required=required
)
def run(self, **kwargs):
args = shlex.split(kwargs['args'])
if self.user is not None:
return sh.sudo('-u', self.user, self.command_name, *args)
else:
return getattr(sh, self.command_name)(*args)
很好,现在更接近了。我们继续引导 AI-6,让它更理解自己的工具架构。
👤 [You]: ok. now that you understand how the CommandTool base class works, check its base class Tool
🤖 [AI-6]: The `Tool` class from which `CommandTool` inherits is an abstract base class (ABC). Here's a breakdown of the `Tool` class:
- **Attributes:**
- `name`: A string that represents the name of the tool.
- `description`: A string that provides a description of the tool.
- `parameters`: A list of `Parameter` objects, each representing a command-line or functional parameter for the tool.
- `required`: A set of strings indicating which parameters are required.
- **Methods:**
- `configure`: This method allows optional configuration of the tool using a provided dictionary. It's intended to be overridden if needed.
- `run`: This is an abstract method that must be implemented by any subclass. It defines how to execute the tool using the given arguments and return the result as a string.
### Next Steps
With this understanding of the `CommandTool` base class and its parent `Tool` class, you can confidently create a new tool for the GitHub CLI by extending `CommandTool`. If you're ready, we can proceed to create the `GhCLI` tool class that utilizes the GitHub CLI (`gh`) for GitHub interactions. Let me know if you want to move forward with this step!
很好。但我不喜欢类名 GhCLI:它暴露了实现细节,也不符合其他工具的命名习惯。
👤 [You]: I want the Github tool class to be called Just Github. The fact it is using gh cli is an implementation detail.
what files or directories need to be created?
🤖 [AI-6]: To create a new command-line tool class named `Github`, you should follow these steps to integrate it into your existing project structure:
Step 1: Determine the File Location
-----------------------------------
Since the existing tools, such as `Ollama`, are located in a specific directory within your project, you should place your new tool in a similar location for consistency and organization.
Let's assume you are following this structure (similar to `Ollama`):
backend/
tools/
base/
ollama/ # Contains the Ollama tool definition
github/ # This is where we will place the new Github tool
Step 2: Create the Necessary Directories and Files
--------------------------------------------------
1. **Create a Directory for the Github Tool:**
- Path: `backend/tools/github/`
- This directory will contain the implementation of the `Github` tool.
2. **Create the `Github` Tool Class File:**
- Path: `backend/tools/github/github.py`
- This file will contain the definition of the `Github` class.
Step 3: Implement the `Github` Tool Class
------------------------------------------
...
这正是我们想听到的:AI-6 已经走在正确道路上。我们继续:
👤 [You]: OK. Show me the Github class
🤖 [AI-6]: Here is the new command-line tool class named `Github`
# backend/tools/github/github.py
from backend.tools.base.command_tool import CommandTool
class Github(CommandTool):
def __init__(self, user: str | None = None):
# Initialize with the command name 'gh' and provide a documentation link
super().__init__(
command_name='gh', user=user,
doc_link='https://cli.github.com/manual/')
def execute(self, args: str) -> str:
"""
Execute any GitHub CLI command by providing the appropriate arguments.
Args:
args: Command-line arguments to pass to the `gh` CLI.
Returns:
The output from the command execution.
"""
return self.run(args=args)
非常好!连文档链接都是对的。但它加了一个多余的 execute() 方法,只是调用 run()。我当然可以自己删掉,但为了保持这个 session 完全交互式,我们让 AI-6 自己把它移除:
👤 [You]: isn't the run() method enough to execute any gh cli command?
🤖 [AI-6]: You're absolutely correct in observing that if you wanted to provide direct access to the `CommandTool`'s `run()` method, you don't strictly need an additional `execute()` wrapper in the `Github` class. The primary role of the `execute()` method was to give a semantic representation or alias to the act of executing a GitHub CLI command, but it indeed adds no extra functionality beyond `run()`.
Thus, in practice, you could simply use the `run()` method directly from instances of the `Github` class for executing commands, since it's already designed to take care of running the CLI with the specified arguments. Here's a revision of the approach without the `execute()` method:
Implementation without `execute()`
----------------------------------
# backend/tools/github/github.py
from backend.tools.base.command_tool import CommandTool
class Github(CommandTool):
def __init__(self, user: str | None = None):
# Initialize with the command name 'gh' and provide a documentation link
super().__init__(
command_name='gh', user=user,
doc_link='https://cli.github.com/manual/')
AI-6 在我们“推一把”之后得出了正确解。随后我继续让它编写测试并执行,AI-6 也成功完成了:测试使用 unittest 框架编写,并通过我们前面看到的 python_test_runner 工具执行。
测试代码在此:
github.com/PacktPublis… 。
这个 session 是 AI-6 如何通过理解自己的代码与架构,为自己添加新能力的一个很好的例子;但它同样展示了 human-in-the-loop 的重要性:引导 AI 系统沿着符合既有架构的方式前进。早期关于 GitHub 工具的建议并非“明显错误”,但它们与现有工具架构的契合度不够好。
下面我们看看:如何在没有 AI-6 帮助的情况下,手动实现一个新工具。
手动实现一个新工具
下一个工具会更复杂一些:我们将构建一个 Claude 工具。它会允许 AI-6 向 Anthropic Claude LLM(claude.ai/)发送推理请求。当 AI-6 正在使用某个 LLM provider(例如 OpenAI)时,它依然可以利用另一个 provider(例如 Anthropic Claude)。Claude 工具实现于 backend/tools/claude_tool.py:它是一个自定义工具,继承 Tool 基类,并使用 Anthropic Python SDK(github.com/anthropics/…)向 Claude 发送请求。我们来看看它的实现。
该工具导入 anthropic 模块(Anthropic Claude 的 Python SDK),以及 AI-6 对象模型中的 Tool 和 Parameter:
import anthropic
from backend.object_model import Tool, Parameter
ClaudeTool 类继承 Tool 基类。它的构造函数比前面工具更复杂:会初始化工具描述、参数列表和 required 参数集合。共有四个参数:
- prompt:发送给 Claude 的提示词/问题。
- model:用于推理的模型,默认是 claude-sonnet-4-20250514。
- max_tokens:生成 token 上限,默认 1000。
- temperature:采样温度(0.0–1.0),默认 0.7。
max_tokens 与 temperature 的默认值来自 Anthropic 文档(docs.anthropic.com/en/docs/abo…)。当 LLM 调用该工具时,它可以通过提供参数来覆盖这些默认值。只有 prompt 是必填的。
注意:这里有一个 self.client 属性,初始化为 None,稍后会用于存放 Anthropic client:
class Claude(Tool):
def __init__(self):
desc = """
Send inference requests to Anthropic Claude LLM. Useful for getting a second opinion or
different perspective from another AI model.
"""
super().__init__(
name='claude',
description=desc,
parameters=[
Parameter(
name='prompt',
type='string',
description='The prompt or question to send to Claude'),
Parameter(
name='model',
type='string',
description="""
See https://docs.anthropic.com/en/docs/about-claude/models/overview.
Defaults to claude-sonnet-4-20250514
"""),
Parameter(
name='max_tokens',
type='integer',
description='Maximum number of tokens to generate. Defaults to 1000'),
Parameter(
name='temperature',
type='number',
description='Temperature for sampling (0.0-1.0). Defaults to 0.7')
],
required={'prompt'}
)
self.client = None
终于,我们看到一个需要配置(configuration)的工具。configure() 方法会从配置字典中读取 Anthropic API key,并在存在时初始化 Anthropic client:
def configure(self, config: dict) -> None:
"""Configure the Claude tool with API key."""
if 'api_key' in config:
self.client = anthropic.Anthropic(api_key=config['api_key'])
run() 方法会发送实际推理请求:先检查 client 是否已初始化;未初始化则返回错误消息。随后从 kwargs 中取出 prompt、model、max_tokens、temperature。注意只有 prompt 是必需的,其余参数都有默认值,因此可选。取到参数后,调用 client.messages.create() 向指定 Claude 模型发送请求,并把响应以字符串形式返回:
def run(self, **kwargs) -> str:
if self.client is None:
return "Error: Claude API key not configured. Set 'api_key' in tool_config for claude tool"
prompt = kwargs['prompt']
model = kwargs.get('model', 'claude-sonnet-4-20250514')
max_tokens = kwargs.get('max_tokens', 1000)
temperature = kwargs.get('temperature', 0.7)
response = self.client.messages.create(
model=model,
max_tokens=max_tokens,
temperature=temperature,
messages=[
{"role": "user", "content": prompt}
]
)
return response.content[0].text
我们可以看到:AI-6 简单的工具接口足以实现各种类型的工具——命令行工具(如文件系统工具)、AI-6 内部工具(如记忆工具)、以及调用外部 API 的工具(如 Claude 工具)。该接口足够灵活,能够容纳不同工具类型及其特定需求。
总结
回顾一下:我们对 AI-6 工具系统做了全面探索,将重点从高层编排引擎转向工具内部结构与行为。我们考察了 AI-6 如何通过统一的通用工具规范,实现“提供商无关”的工具互操作,并能干净地映射到提供商特定格式,例如 OpenAI 的 function calling 或 Ollama 的 tool schema。这些工具的设计目标是可复用、一致、且易于跨不同 LLM API 翻译,其核心是健壮的 schema 与输入校验机制。
随后,我们详细走读了内置命令行工具(如 pwd、ls、awk)以及用于写文件的特殊 echo 工具。这些工具基于 CommandTool 基类实现,能够在 AI 框架内部直接执行文件操作与 shell 交互。进一步的工具(如用于跑 unittest 的 test_runner、用于重启程序的 bootstrap)展示了系统如何支持迭代式自修改。记忆工具(如 list_sessions、load_session、delete_session)则扩展了智能体在受控条件下管理与操控持久会话状态的能力。
接着,我们展示了如何借助 AI-6 自身交互式地开发新工具:通过封装 gh CLI 创建一个 GitHub 工具。这个练习说明了 human-in-the-loop 指导如何与 agentic 推理结合:通过把 AI 系统指向自己的源代码来扩展能力。最后,我们构建了一个基于 API 的 Claude 工具,用于对接 Anthropic LLM,强调 AI-6 如何同时集成并利用多个 LLM provider。总体来看,这些例子凸显了 AI-6 工具系统的强大与灵活。
下一章我们将转向 AI-6 项目的前端部分,介绍面向用户的 Slack 与 Chainlit(Web UI)接口,它们让系统能够以交互式、对话式的方式被控制。