工具函数定义宏
deftool 宏同时生成:
参数声明使用向量格式: [名称 类型 描述]
支持可选参数: [名称 类型 描述 :default 默认值]
支持参数约束: [名称 类型 描述 :enum [..] :pattern ".." :minimum 1 :maximum 9
:min-length n :max-length n :items {:type "string"} :min-items n :max-items n
:properties {…} :required [..] :additional-properties false]
(进 input_schema,模型看得到;调用前按 schema 完整校验,见 tool-args)
对象参数的属性表可写成 [[名称 schema] …] 键值对 vector——保序,字段多于 8 个时用这个写法
(map 字面量多于 8 键会被 Clojure reader 变成 hash map,键序不再跟源码文本一致)。
数组条目的对象(:items {:type "object" :properties …})与更深的嵌套对象都吃同一套写法。
schema 在命名空间加载期构造(不是常量):AOT 编译会把常量 map 重建(>8 键即 PersistentHashMap),参数声明顺序会丢;加载期构造则 eval 与 AOT 完全一致。 支持敏感标记: {:sensitive true} 支持只读 context: {:context true} 支持串行标记: {:serial true}(副作用工具,批内并行时整批退化为按序执行) 支持直接返回: {:return-direct true}(结果即最终答案,不再回灌 LLM)
状态读写(Tool 阶段 MapReduce 契约,见 docs/agent-loop-concurrency-design.md §9):
使用示例:
;; 基本用法 (deftool get-weather "获取指定城市的天气信息" [[city :string "城市名称"]] (fetch-weather city))
;; 带可选参数 (deftool calculate "执行数学计算" [[expression :string "数学表达式"] [precision :int "小数精度" :default 2]] (compute expression precision))
;; 标记敏感工具 (deftool delete-file "删除文件" [[path :string "文件路径"]] {:sensitive true} (io/delete-file path))
;; 带 tags 的工具 (deftool get-weather "获取天气" [[city :string "城市名称"]] {:tags [:weather :external-api :read-only]} (fetch-weather city))
;; 读 context(只读快照)+ 写状态(:writes 声明写意图) (deftool save-note "保存笔记" [[content :string "内容"]] {:context true} ;; ctx 是自动绑定的只读 context 变量 (let [user (context/get-var ctx :user-id)] {:result (str "已保存(" user ")") :writes {:notes content}})) ;; 槽 :notes 的 reducer 决定如何合并
;; 无参数函数 (deftool get-time "获取当前时间" [] (str (java.time.LocalDateTime/now)))
工具函数定义宏
deftool 宏同时生成:
1. 普通 Clojure 函数(可直接调用)
2. 完整的 tool schema 元数据(用于 LLM Function Calling)
参数声明使用向量格式: [名称 类型 描述]
支持可选参数: [名称 类型 描述 :default 默认值]
支持参数约束: [名称 类型 描述 :enum [..] :pattern ".." :minimum 1 :maximum 9
:min-length n :max-length n :items {:type "string"} :min-items n :max-items n
:properties {…} :required [..] :additional-properties false]
(进 input_schema,模型看得到;调用前按 schema 完整校验,见 tool-args)
对象参数的属性表可写成 [[名称 schema] …] 键值对 vector——**保序**,字段多于 8 个时用这个写法
(map 字面量多于 8 键会被 Clojure reader 变成 hash map,键序不再跟源码文本一致)。
数组条目的对象(`:items {:type "object" :properties …}`)与更深的嵌套对象都吃同一套写法。
**schema 在命名空间加载期构造**(不是常量):AOT 编译会把常量 map 重建(>8 键即
PersistentHashMap),参数声明顺序会丢;加载期构造则 eval 与 AOT 完全一致。
支持敏感标记: {:sensitive true}
支持只读 context: {:context true}
支持串行标记: {:serial true}(副作用工具,批内并行时整批退化为按序执行)
支持直接返回: {:return-direct true}(结果即最终答案,不再回灌 LLM)
状态读写(Tool 阶段 MapReduce 契约,见 docs/agent-loop-concurrency-design.md §9):
- 读:{:context true} 工具收到只读的轮初 context 快照(ctx 变量自动绑定)
- 写:任意工具(无论是否声明 :context)返回 {:result r :writes {k v}}
声明写意图;批次屏障处按 tool-call 原始序经槽级 reducer 折叠进 context。
同批工具互相看不到对方的写(快照隔离);失败/超时/拒绝的调用 writes 不生效。
使用示例:
;; 基本用法
(deftool get-weather
"获取指定城市的天气信息"
[[city :string "城市名称"]]
(fetch-weather city))
;; 带可选参数
(deftool calculate
"执行数学计算"
[[expression :string "数学表达式"]
[precision :int "小数精度" :default 2]]
(compute expression precision))
;; 标记敏感工具
(deftool delete-file
"删除文件"
[[path :string "文件路径"]]
{:sensitive true}
(io/delete-file path))
;; 带 tags 的工具
(deftool get-weather
"获取天气"
[[city :string "城市名称"]]
{:tags [:weather :external-api :read-only]}
(fetch-weather city))
;; 读 context(只读快照)+ 写状态(:writes 声明写意图)
(deftool save-note
"保存笔记"
[[content :string "内容"]]
{:context true}
;; ctx 是自动绑定的只读 context 变量
(let [user (context/get-var ctx :user-id)]
{:result (str "已保存(" user ")")
:writes {:notes content}})) ;; 槽 :notes 的 reducer 决定如何合并
;; 无参数函数
(deftool get-time
"获取当前时间"
[]
(str (java.time.LocalDateTime/now)))(build-tool-schema fn-name description parsed-params)构建 Anthropic 兼容的 tool schema
参数:
返回: {:name "函数名" :description "描述" :input_schema {:type "object" :properties {...} :required [...]}}
构建 Anthropic 兼容的 tool schema
参数:
- fn-name: 函数名(keyword)
- description: 函数描述
- params: 解析后的参数列表
返回:
{:name "函数名"
:description "描述"
:input_schema {:type "object" :properties {...} :required [...]}}(call-with-timeout timeout-ms f)在虚拟线程上跑 f,最多等 timeout-ms 毫秒。
返回 [:ok v] | [:err throwable] | [:timeout]——不替调用方决定结果
形状,由调用点自行翻译。这是超时机制的唯一实现,消费者是
chat-client/invoke-tool(强制「工具声明 > 引擎缺省」的生效超时)。
**语义如实声明:超时 = 放弃等待,不是终止执行。**JVM 没有强杀原语
(Thread.stop 已移除),到点只能 interrupt:
Thread/sleep、阻塞 IO(socket read 等)会被真正打断——虚拟线程上 JDK
关闭 socket 并抛 SocketException: Closed by interrupt(JEP 353 起
java.net.Socket 基于 NIO;实测 JDK 25 确认)。故「工具卡在外部 API 上」
这个最常见的形态能干净取消,副作用不落地;InterruptedException 的代码)——它会继续跑完,其副作用可能在超时之后
才落地。这是残余风险,框架消不掉。用虚拟线程而非 clojure.core/future(send-off 平台线程池)的两个理由:
(1) 阻塞 IO 因此真的可取消(平台线程会无视 interrupt 把请求读完);
(2) 被放弃的执行只占几 KB 栈,且不破坏 ToolCallingManager 各引擎的线程模型。
bound-fn* 包装:调用方的动态绑定对 f 照常可见(与 future 的传导语义一致)——
见设计原则 §3「边界内一致」。
在**虚拟线程**上跑 f,最多等 timeout-ms 毫秒。 返回 `[:ok v]` | `[:err throwable]` | `[:timeout]`——**不**替调用方决定结果 形状,由调用点自行翻译。这是超时机制的**唯一实现**,消费者是 `chat-client/invoke-tool`(强制「工具声明 > 引擎缺省」的生效超时)。 **语义如实声明:超时 = 放弃等待,不是终止执行。**JVM 没有强杀原语 (`Thread.stop` 已移除),到点只能 interrupt: - `Thread/sleep`、阻塞 IO(socket read 等)**会被真正打断**——虚拟线程上 JDK 关闭 socket 并抛 `SocketException: Closed by interrupt`(JEP 353 起 `java.net.Socket` 基于 NIO;实测 JDK 25 确认)。故「工具卡在外部 API 上」 这个最常见的形态能干净取消,副作用不落地; - 但**不检查中断标志的代码打不断**(纯 CPU 循环为主,另有 native 调用、吞掉 `InterruptedException` 的代码)——它会继续跑完,其副作用可能在超时**之后** 才落地。这是残余风险,框架消不掉。 用虚拟线程而非 `clojure.core/future`(send-off 平台线程池)的两个理由: (1) 阻塞 IO 因此真的可取消(平台线程会无视 interrupt 把请求读完); (2) 被放弃的执行只占几 KB 栈,且不破坏 ToolCallingManager 各引擎的线程模型。 `bound-fn*` 包装:调用方的动态绑定对 f 照常可见(与 `future` 的传导语义一致)—— 见设计原则 §3「边界内一致」。
(check-timeout! label t)校验 timeout 值,非法则抛 ex-info。label 是出错时点名的对象(如 "工具 :foo")。
DRY:此前 4 处(chat_client.clj validate-tool-timeouts! var/inline + build-chat-client
manager + react.clj check-timeout-opt!)各自手抄 :timeout 必须为正整数毫秒
消息串——消息串被两个模块的测试用正则钉住,改一处漏一处即出问题。
校验 timeout 值,非法则抛 ex-info。label 是出错时点名的对象(如 "工具 :foo")。 **DRY**:此前 4 处(chat_client.clj validate-tool-timeouts! var/inline + build-chat-client manager + react.clj check-timeout-opt!)各自手抄 `:timeout 必须为正整数毫秒` 消息串——消息串被两个模块的测试用正则钉住,改一处漏一处即出问题。
(context-tool? v)检查 tool function 是否需要(只读)context
参数:
返回: boolean
检查 tool function 是否需要(只读)context 参数: - v: var 引用 返回: boolean
(deftool name description params & body)(deftool name description params opts-map & body)定义工具函数
同时生成带 tool schema 元数据的 Clojure 函数。
用法: (deftool 函数名 "描述" [[参数1 :类型 "描述"] [参数2 :类型 "描述" :default 默认值]] 函数体...)
(deftool 函数名 "描述" [[参数1 :类型 "描述"]] {:sensitive true} ; 选项 map(可选) 函数体...)
(deftool 函数名 "描述" [[参数1 :类型 "描述"]] {:context true} ; 需要只读 context ;; ctx 变量自动可用(只读快照);写状态用 :writes {:result "..." :writes {:key val}})
生成的 var 元数据:
定义工具函数
同时生成带 tool schema 元数据的 Clojure 函数。
用法:
(deftool 函数名
"描述"
[[参数1 :类型 "描述"] [参数2 :类型 "描述" :default 默认值]]
函数体...)
(deftool 函数名
"描述"
[[参数1 :类型 "描述"]]
{:sensitive true} ; 选项 map(可选)
函数体...)
(deftool 函数名
"描述"
[[参数1 :类型 "描述"]]
{:context true} ; 需要只读 context
;; ctx 变量自动可用(只读快照);写状态用 :writes
{:result "..." :writes {:key val}})
生成的 var 元数据:
- :tool/schema Anthropic 格式 tool schema(**加载期**构造,eval / AOT 一致)
- :tool/params 参数定义列表
- :tool/sensitive 是否为敏感工具
- :tool/serial 是否串行工具(批内并行时整批退化)
- :tool/timeout 超时毫秒(可选)。经 chat-client 调用时**开箱即生效**(缺省不超时;
优先级:本声明 > 引擎 {:timeout ms})。语义为「放弃等待」而非
「终止执行」——超时后工具可能仍在后台跑完并产生外部副作用
(见 call-with-timeout 文档)
- :tool/category 工具分类
- :tool/context 是否需要(只读)context
- :tool/tags 标签集合(用于过滤)
- :tool/function 标记为 tool function(get-schema v)获取 tool function 的 tool schema
参数:
返回: tool schema map 或 nil
获取 tool function 的 tool schema 参数: - v: var 引用 返回: tool schema map 或 nil
(get-tags v)获取 tool function 的标签集合
参数:
返回: 标签集合(set)或 nil
获取 tool function 的标签集合 参数: - v: var 引用 返回: 标签集合(set)或 nil
(has-any-tag? v tags)检查 tool function 是否包含任意一个指定标签
参数:
返回: boolean
检查 tool function 是否包含任意一个指定标签 参数: - v: var 引用 - tags: 标签集合 返回: boolean
(invoke v args)(invoke v args context)执行 tool function 并返回标准结果
参数:
返回: {:success bool :result any :error string (:writes {k v})} 工具返回含 :writes 的 map({:result r :writes {k v}})时拆出写意图, 由批次屏障处的 reducer 折叠进共享 context(工具本身不改 context)。
执行 tool function 并返回标准结果
参数:
- v: tool function 的 var 引用
- args: 参数 map
- context: (可选) Context 对象({:context true} 工具的只读快照)
返回:
{:success bool :result any :error string (:writes {k v})}
工具返回含 :writes 的 map({:result r :writes {k v}})时拆出写意图,
由批次屏障处的 reducer 折叠进共享 context(工具本身不改 context)。(params->json-schema parsed-params)将解析后的参数列表转换为 JSON Schema
properties 按参数声明顺序写出(>8 个参数也保序,进 prompt cache 的稳定前缀)。
:properties / :items 里的嵌套 map 原样透传;嵌套属性表(含 :items 里数组条目的对象、
更深的嵌套对象)也可写成键值对 vector([[:字段 {…}] …])以保序——map 字面量 >8 键受
Clojure reader 限制,键序不是源码文本序。
返回: {:type "object" :properties {:param1 {:type "string" :description "..."} ...} :required ["param1" ...]}
将解析后的参数列表转换为 JSON Schema
properties 按参数声明顺序写出(>8 个参数也保序,进 prompt cache 的稳定前缀)。
`:properties` / `:items` 里的嵌套 map 原样透传;嵌套属性表(含 `:items` 里数组条目的对象、
更深的嵌套对象)也可写成键值对 vector(`[[:字段 {…}] …]`)以保序——map 字面量 >8 键受
Clojure reader 限制,键序不是源码文本序。
返回:
{:type "object"
:properties {:param1 {:type "string" :description "..."} ...}
:required ["param1" ...]}(retry-spec v)读取 tool function 的 :retry 声明(deftool 选项,幂等工具 opt-in)。
返回: nil(未声明)| true | {:max-retries n :initial-delay-ms ms}
读取 tool function 的 :retry 声明(deftool 选项,幂等工具 opt-in)。
返回: nil(未声明)| true | {:max-retries n :initial-delay-ms ms}(return-direct-tool? v)检查 tool function 是否声明 :return-direct(结果即最终答案,不再回灌 LLM)
参数:
返回: boolean
检查 tool function 是否声明 :return-direct(结果即最终答案,不再回灌 LLM) 参数: - v: var 引用 返回: boolean
(serial-tool? v)检查 tool function 是否声明 :serial(副作用工具; 批内并行时含 serial 工具的整批退化为按序执行)
参数:
返回: boolean
检查 tool function 是否声明 :serial(副作用工具; 批内并行时含 serial 工具的整批退化为按序执行) 参数: - v: var 引用 返回: boolean
(timeout-spec v)读取 tool function 的 :timeout 声明(deftool 选项,毫秒)。
与 :serial / :retry / :return-direct 同款——经 chat-client 调用时开箱即生效,
由 chat-client/invoke-tool 强制。缺省不超时;引擎可给整体缺省
(…-tool-calling-manager {:timeout ms}),本声明恒优先。
内联工具的同名声明经 chat-client/tool-timeout 读取(本函数只管 var)。
语义注意:JVM 上超时 = 放弃等待 ≠ 终止执行,详见 call-with-timeout。
返回: nil(未声明)| 正整数毫秒
读取 tool function 的 :timeout 声明(deftool 选项,毫秒)。
与 `:serial` / `:retry` / `:return-direct` 同款——经 chat-client 调用时**开箱即生效**,
由 `chat-client/invoke-tool` 强制。缺省不超时;引擎可给整体缺省
`(…-tool-calling-manager {:timeout ms})`,本声明恒优先。
内联工具的同名声明经 `chat-client/tool-timeout` 读取(本函数只管 var)。
语义注意:JVM 上超时 = 放弃等待 ≠ 终止执行,详见 `call-with-timeout`。
返回: nil(未声明)| 正整数毫秒(tool-function? v)检查 var 是否为 tool function
参数:
返回: boolean
检查 var 是否为 tool function 参数: - v: var 引用(如 #'get-weather) 返回: boolean
(tool-meta fn-name description params)运行时构建 deftool 的元数据片段:{:tool/schema … :tool/params […]}
为什么不能把 schema 当常量:deftool 过去把 schema 作为常量塞进 var 元数据。 AOT 编译时 Clojure 会把常量 map 重建(>8 键即 PersistentHashMap),参数声明顺序 全丢——单测在 eval 下是绿的,发出去的包里模型看到的顺序却是乱的(见 feedbacks/2026-11-11-deftool-order-lost-under-aot.md)。
改为命名空间加载期构造:eval 与 AOT 走同一条路径,顺序由 ordered-map 钉住。
参数:
返回: {:tool/schema … :tool/params […]}
运行时构建 deftool 的元数据片段:{:tool/schema … :tool/params […]}
**为什么不能把 schema 当常量**:deftool 过去把 schema 作为常量塞进 var 元数据。
AOT 编译时 Clojure 会把常量 map 重建(>8 键即 PersistentHashMap),参数声明顺序
全丢——单测在 eval 下是绿的,发出去的包里模型看到的顺序却是乱的(见
feedbacks/2026-11-11-deftool-order-lost-under-aot.md)。
改为**命名空间加载期**构造:eval 与 AOT 走同一条路径,顺序由 `ordered-map` 钉住。
参数:
- fn-name: 工具名(keyword)
- description: 工具描述
- params: 参数声明向量(用户写法;参数名是符号)
返回: {:tool/schema … :tool/params […]}(valid-retry? r):retry 声明是否合法:nil(未声明)| true(opt-in 用默认)| 正整数 map。
与 valid-timeout? 对称——:retry 此前零校验,{:max-retries "3"} 在
运行期 (long ...) 抛 CCE 且被收敛成指向工具的 :semantic 错误——症状偏离病因。
`:retry` 声明是否合法:nil(未声明)| true(opt-in 用默认)| 正整数 map。
与 `valid-timeout?` 对称——`:retry` 此前零校验,`{:max-retries "3"}` 在
运行期 `(long ...)` 抛 CCE 且被收敛成指向工具的 :semantic 错误——症状偏离病因。(valid-timeout? t):timeout 声明是否合法:nil(未声明)或正整数毫秒。
非正整数一律拒绝——"5s" 会在 deref 处抛 ClassCastException,-1 会让该
工具每次调用都立刻超时(静默、无从排查),2.7 会被静默截断。由
chat-client/build-chat-client 在装配期校验(var 与内联工具汇合、尚未开始执行的最早时点)。
`:timeout` 声明是否合法:nil(未声明)或正整数毫秒。 非正整数一律拒绝——`"5s"` 会在 deref 处抛 ClassCastException,`-1` 会让该 工具每次调用都立刻超时(静默、无从排查),`2.7` 会被静默截断。由 `chat-client/build-chat-client` 在装配期校验(var 与内联工具汇合、尚未开始执行的最早时点)。
(validate-args v args)按 var 元数据中的 input_schema 完整校验工具参数(type / enum / pattern /
min·max / minLength / items / required …,一次报全,见 tool-args)。
参数:
返回: {:valid bool :errors [string]}——每处错误一行(带路径与期望)
按 var 元数据中的 input_schema **完整**校验工具参数(type / enum / pattern /
min·max / minLength / items / required …,一次报全,见 `tool-args`)。
参数:
- v: tool function 的 var 引用
- args: 参数 map
返回:
{:valid bool :errors [string]}——每处错误一行(带路径与期望)cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |