夜雨聆风学习资料网

ARTICLE · 1014056

AI Harness 工具(tool)执行引擎:一切皆可为工具

AI Harness 工具(tool)执行引擎:一切皆可为工具

👉目录

1 大多数框架,把工具调用仅作为一个函数

2 工具的定义:Definition 与参数 Schema

3 工具的注册:Registry 与 Register

4 工具的调用与执行:Call、Result 与两层 Execute

5 函数调用(Function Calling)的本质与原理

6 多模型适配:一套工具,适配所有模型

7 一切皆工具:MCP、记忆、技能,都是工具

8 完整示例:从读到响应的完整链路

9 写在最后

关注JeffckyShare,一手技术干货提前解锁👇

如果要给 AI Agent 下一个最简的定义,一个广为流传的公式是:Agent = LLM + Tool + Memory。

LLM 是大脑,负责理解和推理;Memory 是记忆,负责记住上下文;Tool 是手脚,负责真正动手——读文件、跑命令、发请求、查数据。

这个公式里,大脑和记忆被聊得最多,唯独手脚这一环,很多人以为就是调个函数就够了,可偏偏是这一环决定了 Agent 到底是真能干活,还是只会说、不会做。

这篇文章就专讲 Tool 这一环,以及撑起它的那一整套工具执行引擎。

Function Calling,就是模型侧的那只手:模型输出一句结构化的「我想调用这个函数,参数是这些」。

而工具执行引擎,是 Harness 侧的那只手:它收到这个调用请求,真正去执行——查注册、过治理、跑逻辑、返回结果。

两者一体两面:Function Calling 负责决定调用什么,工具执行引擎负责把调用真正落地,这篇文章,讲的就是这套机制。

01

大多数框架,把工具调用仅作为一个函数

先看主流框架是怎么定义工具的。

从 OpenAI 的 Function Calling,到 Anthropic 的 Tool Use,再到 LangChain 的 @tool的工具机制,本质上是同一条流水线:

定义 Schema → 模型输出调用 → 你的代码执行 → 返回结果。

这条流水线很成熟,也够用——但它的关注点,几乎全在怎么让模型调用、怎么定义、怎么执行上,漏掉了更关键的一层:执行之前的安全。

换句话说,主流框架定义怎么调用工具,但未进一步阐述怎么安全地调用工具,具体缺四样:

缺拦截。

模型说调就调,中间没有一层能拦下来做校验、限流、改写,参数错了、越权了,只能眼睁睁看它执行。

缺治理。

工具是静态定义、全部暴露的,没有「哪些工具在当前场景允许用」的策略——被禁的工具,模型照样看得见、调得了。

缺审批。

没有 human-in-the-loop,危险的调用,没有一个「挂起等人类批准」的机制。

缺审计。

执行完就完了,谁、在什么时候、调了什么、结果如何,没有记录,出了事没法追。

而这四道闸——拦截、治理、审批、审计——靠的是 hook 管道 + 审批机制,这套东西在系列前面系列片篇《Hook 管道引擎》、《AI Harness 动态治理引擎》《AI Harness 安全治理终章》里已经讲述了核心,这里不再重复。

本文要讲的,是工具执行引擎的另一半:工具怎么定义、怎么注册、怎么执行、怎么适配所有模型——执行机制本身。

这才是工具执行引擎和函数调用(Function Calling)的根本区别:函数调用是能调,工具执行引擎是既敢调、又调得对、还换模型不换工具。

我们工具执行引擎要做的是把执行这一层做成一套可注册、可拦截、可治理、可适配的完整机制。

02

工具的定义:Definition 与参数 Schema

一个工具,先得把它是什么说清楚,落到具体实现上就是 Definition——工具定义,直接看它的 Go 结构:

type Definition struct {    Name        string      `json:"name"`    Description string      `json:"description"`    Display     string      `json:"display,omitempty"`    Parameters  []Parameter `json:"parameters"`}

三个字段各司其职:Name 是工具名(模型调用时喊的名字),Description 是给模型看的说明(模型靠它决定什么时候用这个工具),Display 是给人看的展示模板(可选),Parameters 是参数列表。

其中参数,又是单独一个结构:

type Parameter struct {    Name        string      `json:"name"`    Type        ParamType   `json:"type"`    Description string      `json:"description"`    Required    bool        `json:"required"`    Enum        []string    `json:"enum,omitempty"`    Minimum     *float64    `json:"minimum,omitempty"`    Maximum     *float64    `json:"maximum,omitempty"`    Pattern     string      `json:"pattern,omitempty"`    Items       *ParameterSchema `json:"items,omitempty"`    Properties  map[string]*ParameterSchema `json:"properties,omitempty"`}

Parameter 里有两个字段指向 ParameterSchema,Items 和 Properties,它们正是用来嵌套的。

当参数是数组时,Items 描述数组元素长什么样。

当参数是对象时,Properties 描述对象的每个属性长什么样。

而 ParameterSchema,就是嵌套参数的那个结构:

type ParameterSchema struct {    Type        ParamType                    `json:"type"`    Description string                       `json:"description,omitempty"`    Enum        []string                     `json:"enum,omitempty"`    Minimum     *float64                     `json:"minimum,omitempty"`    Maximum     *float64                     `json:"maximum,omitempty"`    Pattern     string                       `json:"pattern,omitempty"`    Properties  map[string]*ParameterSchema  `json:"properties,omitempty"`    Items       *ParameterSchema             `json:"items,omitempty"`    Required    []string                     `json:"required,omitempty"`}

两个结构几乎一样,区别只有两处——Parameter 描述的是有名字的顶级参数,ParameterSchema 描述的是嵌套的、没有名字的参数:

  • Parameter 有 Name 字段,ParameterSchema 没有——因为嵌套参数的名字就是 map 的 key(对象属性名)或数组下标,不需要单独的 Name。
  • Parameter的Required是bool(这个参数本身是否必填),ParameterSchema 的 Required 是 []string(当它是 object 时,记录哪些属性必填),这正是 JSON Schema 里 object 的 required 字段是数组的规范。

每个参数有类型(string / number / boolean / object / array)、描述、是否必填,还有一串约束字段——枚举值(Enum)、数值范围(Minimum / Maximum)、正则(Pattern)。

如果参数是数组或对象,还能嵌套 Items 和 Properties,往下一直套娃。

这套东西,就是 JSON Schema 的一个子集,它有两个用途:给模型看(让模型知道能传什么),给引擎用(做参数校验的依据)。

也正因为它就是 JSON Schema,后面讲 strict 模式时你会发现,它俩能直接对上。

Definition是工具的说明书,Name 是名字,Description 给模型看,Parameters 是一套带约束的 JSON Schema 子集。

03

工具的注册:Registry 与 Register

定义有了,还得把怎么干活绑上去,再挂进一张表里,这就是注册。

干活的那个函数叫 Handler,签名很简单:

type Handler func(ctx context.Context, args json.RawMessage) (string, error)

入参是参数 JSON,出参是内容加错误,Definition 是说明书,Handler 是干活的手。

注册表 Registry,本质上是一张 map——工具名 → 定义 + 处理函数:

type Registry struct {    mu    sync.RWMutex    tools map[string]registration}type registration struct {    Definition Definition    Handler    Handler    CreatedAt  time.Time}

注册动作,就是 Register 这个函数:

func(r *Registry) Register(def Definition, handler Handler) error {    if def.Name == "" {        return errs.Newf(errs.CodeCapability, "tools.register", "tool name cannot be empty")    }    if handler == nil {        return errs.Newf(errs.CodeCapability, "tools.register", "tool handler cannot be nil")    }    r.mu.Lock()    defer r.mu.Unlock()    if _, exists := r.tools[def.Name]; exists {        return errs.Newf(errs.CodeCapability, "tools.register", "tool %q already registered", def.Name)    }    r.tools[def.Name] = registration{        Definition: def,        Handler:    handler,        CreatedAt:  time.Now(),    }    return nil}

注册时,三道校验一个不能少:name 不能为空、handler 不能为 nil、同名工具不能重复注册。

都过了,才把 Definition 和 Handler 绑成一个 registration,挂进 map。

有了 Register,自然还有 Replace(覆盖同名)和 Unregister(注销)等等——增删改,至此一套已齐活。

注册 = 把 Definition 和 Handler 绑在一起,挂进 map;Register 三道校验(name 非空、handler 非 nil、不重复),一个都不能少。

04

工具的调用与执行:Call、Result 与两层 Execute

工具注册好了,模型就能调了,调用进来的是一个 Call,执行完出去的是一个 Result:

type Call struct {    ID        string          `json:"id"`    Name      string          `json:"name"`    Arguments json.RawMessage `json:"arguments"`}type Result struct {    CallID  string `json:"call_id"`    Name    string `json:"name"`    Content string `json:"content"`    IsError bool   `json:"is_error"`}

Call 回答"想调什么"——一个函数名加一段参数 JSON。

Result 回答"干成了什么"——一段内容加一个是否出错的标记。

真正执行的,是 Execute,它分两层。

外层,是 InterceptedExecutor——拦截器链的装饰器:

func(e *InterceptedExecutor) Execute(ctx context.Context, call Call) Result {    for _, ic := range e.interceptors {        if err := ic.Before(ctx, &call); err != nil {            return Result{CallID: call.ID, Content: err.Error(), IsError: true}        }    }    result := e.inner.Execute(ctx, call)    for i := len(e.interceptors) - 1; i >= 0; i-- {        e.interceptors[i].After(ctx, &call, &result)    }    return result}

Before 链按顺序跑,任何一个返回错误就短路(不执行,直接当 IsError 返回);全过了才调 inner;After 链按逆序跑,做善后。

内层,是 Registry.Execute——真正干活:

func(r *Registry) Execute(ctx context.Context, call Call) (result Result) {    reg, exists := r.tools[call.Name]    if !exists {        return Result{CallID: call.ID, Name: call.Name, Content: "tool not found", IsError: true}    }    if !r.allowed(call.Name) {        return Result{CallID: call.ID, Name: call.Name, Content: "denied by policy", IsError: true}    }    output, err := reg.Handler(ctx, call.Arguments)    if err != nil {        return Result{CallID: call.ID, Name: call.Name, Content: "tool error", IsError: true}    }    return Result{CallID: call.ID, Name: call.Name, Content: output, IsError: false}}

四步,一条直线:查表(不在 → not found)→ 过治理(不允许 → denied by policy)→ 调 handler(出错 → tool error)→ 返回结果。

每一个失败分支,都返回 IsError: true。

这里要顺带厘清一个关系,避免和前面文章的Hook 管道混淆。

拦截器链的 Before/After,是一套通用的插桩机制——它只定义执行前后能挂东西,不定义挂的是什么。

而Hook 管道(tool.pre / tool.post)正是这套机制的一个实现:hooks.Pipeline 实现了 Interceptor 接口,它的 Before 就是去派发 tool.pre 事件。

所以两者是插座和插头的关系,拦截器链是插座,Hook 管道是插在上面的一个插头。

本文重点是插座本身(执行机制),插头(治理逻辑)前面文章已经讲透,这里不展开。

Call 是想调什么、Result 是干成了什么;Execute 是两层——外层拦截器装饰(Before 顺序、After 逆序),内层查表 → 过治理 → 调 handler → 返回,每个失败分支都 IsError。

05

函数调用(Function Calling)的本质与原理

那 Function Calling 到底是什么?

先从形说起,Function Calling 在传输上,就是一套 JSON 往返协议,分三段:

  • 工具定义
    (引擎 → 模型):一段 JSON Schema,告诉模型有哪些函数、收什么参数;
  • 调用请求
    (模型 → 引擎):一段 JSON,模型声明我要调这个函数、参数是这些;
  • 工具结果
    (引擎 → 模型):一段 JSON,把执行结果还回去。

三段全是 JSON,一次往返,所以很多人把它理解成一种JSON 协议,工程上也完全成立。

但神要再深一层:JSON 只是载体,真正变的是模型的能力——它从只会输出自由文本,升级到了能输出结构化、可被程序解析的内容。

这个能力跃迁,才是 Function Calling 的本质;JSON 只是这个能力的落地格式。

有了这层理解,再看它的本质,一句话:模型不执行函数,它只声明想调用哪个函数、传什么参数。

而声明这件事,靠的是一个关键信号——finish_reason。

finish_reason:模型的暂停信号。

每次模型响应,都会带一个 finish_reason,告诉调用方我为什么停下来了,以 DeepSeek 官方文档为例,它有这几个值:

finish_reason
含义
是最终答案吗
stop
自然说完,或命中 stop 序列
是
length
撞上输出上限 / 上下文边界,被截断
否
content_filter
内容被审查拦截
否
tool_calls
模型想先调一个工具,还没说完
否
insufficient_system_resource
推理资源不足,被打断
否

关键就在 tool_calls 这个值:它不是结束,而是暂停,模型在说我要回答你,但得先调某个工具,等结果回来再继续说。

所以引擎拿到响应,第一件事就是看 finish_reason:是 tool_calls,就去执行工具;是 stop,就是最终答案。

这一步,正是后面完整示例里那个 switch 在做的事。

strict 模式:参数校验的模型侧前置。

模型产出的参数,不一定合法,DeepSeek 官方文档自己也提醒:模型不总是生成合法 JSON,可能幻觉出 schema 里没有的参数。

为此,DeepSeek 提供了一个 strict 模式:开启后,模型输出 Function 调用时会严格遵循你定义的 JSON Schema,不再凭空捏造参数。

它支持的 Schema 子集——object / string / number / integer / boolean / array / enum / anyOf,外加 pattern、format、minimum、maximum、multipleOf、additionalProperties:false

正好和我们的工具定义里那套参数 Schema 一一对应。

换句话说,ToOpenAIFormat 把工具定义转成 function calling 格式时,就是在把这份 Schema 喂给 strict 模式做服务端强校验。

所以参数校验其实是双保险:模型侧靠 strict 模式减少幻觉,引擎侧靠 pre 钩子和治理再拦一道,模型别乱说,引擎别乱做。

JSON Output:模型侧的不确定性。

另一个模型侧的事实是:DeepSeek 的 JSON Output 功能,官方明确提示有概率返回空 content。

也就是说,模型报了 tool_calls,结果 tool_calls 里可能解析出来是空的。

这就解释了为什么引擎里要有这种防御——当 finish_reason = tool_calls 但解析出来却是空的,不能当成功,得当成退化响应处理掉。

一句话收尾:Function Calling本身就不完全可靠——参数会幻觉、结果会为空。

 所以它只能是意图,真正的可靠性,得靠引擎侧的校验、治理、兜底来补。

Function Calling 是模型侧的意图声明,工具执行引擎是 Harness 侧的真实执行;模型侧会幻觉、会为空,所以执行的可靠性只能靠引擎兜底。

06

多模型适配:一套工具,适配所有模型

那把工具包一层,到底图什么?

四个目的:统一抽象(隔离执行细节)、多模型适配(换模型不改定义)、拦截治理(Before/After 插桩)、统一数据流(四个对象一条流)。

其中最能体现价值、也最容易被忽视的,是多模型适配。

工具定义,是模型无关的。

回头看 02 节的 Definition——name、description、parameters,这里面没有任何一个字段是 OpenAI 专属的,也没有 Anthropic 专属的。它是一份中性的描述。

ToOpenAIFormat:把定义翻译成普通话。

但模型要的是它自己协议里的格式,所以引擎先把 Definition 转成 OpenAI 的 function calling 格式,作为中间交流桥梁的普通话。

Provider 层:把普通话翻译成各家方言。

 真正的适配在 completion 包。它定义了一个 Provider 接口(Complete / CompleteStream / SetFallbacks),把调模型这个动作抽象掉,每个厂商一个实现:

  • OpenAI:原生实现;
  • DeepSeek、Kimi、智谱、Copilot:因为它们都兼容 OpenAI 协议,所以不单独写实现,而是注册一个别名,直接指向 OpenAI 的实现——一行代码完成适配;
  • Anthropic、Gemini:协议不同,需要双向转换——出方向把统一请求转成它的格式,入方向把响应转回来。

所以 DeepSeek 的适配,答案就是这么简单:它兼容 OpenAI,注册一个别名就够了,根本不用单独写 provider。

一个 read_file,三种形态。

把这个翻译过程,用 read_file 工具具体走一遍,注册时,它长这样(02 节讲过 Definition):

def := tools.Definition{    Name:        "read_file",    Description: "读取指定路径的文件内容",    Parameters: []tools.Parameter{        {Name: "path", Type: tools.TypeString, Description: "文件路径", Required: true},    },}

注意一件事:这里的 Parameters 是个切片(一个数组,每个元素是"参数名 + 类型 + 是否必填")。

但各家模型要的不是切片,而是一个 JSON Schema 对象——{type: "object", properties: {...}, required: [...]},两者结构完全不同,所以中间必须先做一次转换。

这次转换是 ToOpenAIFormat 的职责:遍历切片,把每个参数转成 properties 里的一个 key,把必填参数名收集进 required,最后包成一个标准 JSON Schema 对象:

properties := make(map[string]any)required := []string{}for _, p := range def.Parameters {    properties[p.Name] = map[string]any{        "type": string(p.Type), "description": p.Description,    }    if p.Required { required = append(required, p.Name) }}// 结果包成:{ "type": "object", "properties": properties, "required": required }
转完,就得到了 OpenAI function calling 需要的"普通话"——parameters 字段就是那个 JSON Schema 对象:
{    "type": "function",    "function": {        "name": "read_file",        "description": "读取指定路径的文件内容",        "parameters": {            "type": "object",            "properties": {                "path": {"type": "string", "description": "文件路径"}            },            "required": ["path"]        }    }}
DeepSeek、Kimi、智谱因为兼容 OpenAI,直接用这份格式,一行别名搞定:
RegisterProvider(&providerAlias{name: "deepseek", factory: f}) // f 是 openai 工厂
而 Anthropic 协议不同,但它要的 input_schema 本来也是一个 JSON Schema 对象——和 OpenAI 的 parameters 是同一个东西。
所以适配层几乎不用转换,只是把字段名从 parameters 换成 input_schema:
{    "name": "read_file",    "description": "读取指定路径的文件内容",    "input_schema": {        "type": "object",        "properties": {            "path": {"type": "string", "description": "文件路径"}        },        "required": ["path"]    }}

看input_schema 里的内容和 parameters 一模一样,只是外层字段名换了。

所以真正的转换只有一步:切片 → JSON Schema 对象(ToOpenAIFormat 做)。这一步做完,OpenAI 和 Anthropic 都只是换个字段名包一层。

同一个 Definition,三种形态,但无论换哪个模型,注册表里那份 Definition 一行都不用改,定义只写一次,翻译交给适配层。

finish_reason,也要归一化。

各家的 finish_reason 还不一样

OpenAI 是 stop / length / tool_calls / content_filter

Anthropic是 end_turn / max_tokens / tool_use / stop_sequence

DeepSeek 又多了一个 insufficient_system_resource。

适配层的职责,就是把这些方言统一映射成一套通用的值,让 turn 循环里的 switch 只需要守一套。

厂商
各自的值
归一后
OpenAI
stop / length / tool_calls / content_filter
直接对应
Anthropic
end_turn / max_tokens / tool_use / stop_sequence
stop / length / tool_calls / stop
DeepSeek
上述 + insufficient_system_resource
漏进 default 兜底
这样换模型这件事,就变成了换一个 provider 实现,工具定义一行不用改。

07

一切皆工具:MCP、记忆、技能,都是工具
一切皆工具这句话,在实现里不是比喻,而是实际字面意义。
MCP、记忆、技能,统统走同一个 registry.Register,注册成同一张表里的工具,看三个真实的例子。
MCP:把外部服务搬进注册表。
连接 MCP 服务器,发现它提供的工具,逐个转成 Definition + handler,注册进表:
client, _ := NewClient(ctx, name, transport)toolDefs, _ := client.ListTools(ctx)   // 发现 MCP 服务器上的工具for _, td := range toolDefs {    def := tools.Definition{        Name:        "mcp__" + name + "_" + td.Name,    // mcp__ 前缀        Description: "[MCP:" + name + "] " + td.Description,        Parameters:  convertInputSchema(td.InputSchema), // Schema 也转成我们的 Parameter    }    handler := func(ctx context.Context, args json.RawMessage) (string, error) {        result, err := client.CallTool(ctx, td.Name, params) // 转发给 MCP 客户端        if err != nil {            return "", err        }        var text string        for _, item := range result.Content {           // Content 是 []sdk.Content            if tc, ok := item.(*sdk.TextContent); ok {   // 只取文本块,拼成字符串                text += tc.Text            }        }        return text, nil    }    registry.Register(def, handler)}

这段代码用的是官方 MCP Go SDK(github.com/modelcontextprotocol/go-sdk v1.7.0,支持 2025-06-18 新版协议)。

注意两个新版细节:result.Content 是 []sdk.Content 接口切片,要逐个断言成 *sdk.TextContent 才能取文本;

而 MCP 新版协议里 CallToolResult 其实还多了一个 structuredContent 字段,可以返回结构化数据——我们这里只取了文本部分,够用。

工具名加 mcp__ 前缀(比如 mcp__github_list_issues)不是为了好看,而是为了治理:policy 里写一条 mcp__* 就能把整个 MCP 服务器的工具一网打尽。

而 handler 干的是转发——所以对模型来说,调用一个 MCP 工具和调用一个原生工具,没有任何区别。

记忆:读写记忆,也是两个工具。

记忆系统的读写,同样不是特殊能力,memory_read 和 memory_write,就是两个注册在表里的普通工具:
funcRegisterMemoryTools(registry *tools.Registry, m *Memory) error {    registry.Register(memoryReadDef(), memoryReadHandlerFacade(m))   // memory_read    registry.Register(memoryWriteDef(), memoryWriteHandlerFacade(m)) // memory_write    return nil}
handler 背后是 Memory 门面——工具只是门面的一个入口,真正的存储、检索、冲突检测,全在门面后面。
技能:一个技能,就是一个返回操作说明的工具。
技能的注册最妙,每个 skill 注册成一个工具,handler 不干别的,直接把 skill 的完整内容返回给 LLM:
funcregisterOne(registry *tools.Registry, sk SkillDef) error {    def := tools.Definition{        Name:        sk.Name,        Description: "Invoke the " + sk.Name + " skill. " + sk.Description,        Parameters:  []tools.Parameter{},   // 无参数    }    handler := func(ctx context.Context, args json.RawMessage) (string, error) {        return sk.Body, nil   // 直接返回 skill 的指令 + 内嵌工具/hook 定义    }    return registry.Register(def, handler)}

所以调用一个技能,本质是调一个工具,把这个技能的操作步骤拿回来,照着做。

三种来源,同一个入口,模型看到的世界,就是一张统一的工具表——无论能力来自原生代码、外部 MCP 服务器、记忆系统、还是技能文件,对模型来说都是一个能调用的函数。

MCP、记忆、技能,都是注册表里的普通工具——Definition 说是什么、Handler 说怎么干,来源不同,入口相同。

08

完整示例:从读到响应的完整链路

把前面所有东西串起来,看一次读取文件从用户输入到最终响应的完整链路。

用户问:读取 /tmp/note.txt 的内容。

第一次请求(带上工具定义):
{ "model": "deepseek-chat",  "messages": [ { "role": "user", "content": "读取 /tmp/note.txt 的内容" } ],  "tools": [ /* read_file 定义 */ ] }

第一次响应(finish_reason = tool_calls):

{ "choices": [ { "finish_reason": "tool_calls",    "message": { "role": "assistant", "content": null,      "tool_calls": [ { "id": "call_abc123", "type": "function",        "function": { "name": "read_file", "arguments": "{\"path\": \"/tmp/note.txt\"}" } } ] } } ] }

两个细节:finish_reason 是 tool_calls、content 是 null——模型这轮没说话,只发出了调用指令;arguments 是 JSON 字符串(外面那层引号),不是对象。

引擎拿到响应,第一件事是看 finish_reason,是 tool_calls,就走工具分支:

  • 解出 tool_calls,构造 tools.Call{ID, Name, Arguments};
  • 并发执行:每个调用先过 tool.pre 钩子(block / pending→审批 / modify 改参),再 executor.Execute;
  • 执行 read_file,拿到结果"你好世界"。
回填结果,第二次请求(关键动作):
{ "model": "deepseek-chat",  "messages": [    { "role": "user", "content": "读取 /tmp/note.txt 的内容" },    { "role": "assistant", "content": null, "tool_calls": [ /* 原样返回 */ ] },    { "role": "tool", "tool_call_id": "call_abc123", "content": "你好世界" } ],  "tools": [ /* read_file 定义 */ ] }

这里有个硬约定:role:tool 的 tool_call_id 必须和上一轮 tool_calls[0].id 完全对应,模型靠这个 id 才知道结果对应哪个调用。

第二次响应(finish_reason = stop):

{ "choices": [ { "finish_reason": "stop",    "message": { "role": "assistant", "content": "文件 /tmp/note.txt 的内容是:你好世界" } } ] }

这次 finish_reason 是 stop、content 有值,就是最终答案,循环结束。

整个链路的本质,是一个产出调用 → 执行 → 回填 → 再推理的循环,直到模型不再要求调工具为止。

一次完整的工具调用,不是调一次就结束,而是请求 → tool_calls → 执行 → role:tool 回填 → 再请求 → stop的循环,直到模型给出最终答案。

09

写在最后

回到标题那句话:一切皆工具。

读文件、跑命令、发 HTTP,是工具;MCP、记忆、技能,也是工具。

在AI Harness 里,所有 AI 能动手的能力,最终都收敛成注册表里的一个 Definition + 一个 Handler,模型看到的世界,就是这一张统一的工具表。

而这张表要能放心用,靠的是它背后的两层执行、多模型适配,以及 Hook + 审批的治理。

它们合起来,才让 Function Calling 从一句不可靠的意图,变成一次受控的、可审计的真实执行。

所以,当你给AI 接工具时,真正的工程难点从来不是「让模型能调用函数」,而是「让这次调用值得被信任」。

前者是 Function Calling,后者是工具执行引擎。

而治理负责兜底里的治理——策略、pre/post 钩子、审批、审计——本质就是我们前面系列反复讲的 hook 管道 + 审批机制。

模型负责说,引擎负责做,治理负责兜底——三者合起来,AI 才算真正有了一双能被信任的手。

以上全文基于个人理解,欢迎批评指正。

AI Harness 上下文工程:你可能只看到了冰山一角
AI Harness 安全治理终章:构建加密审批门控
AI Harness 动态治理引擎:为什么是Starlark?
Hook 管道引擎 · 十二事件拦截 · 四动作执法 — AI Harness 运行时护栏设计
双循环引擎 · 四层编译门禁 · 纵深防御沙箱 — 可治理 AI Harness 运行时架构

相关学习资料