ChatModel —— 一次 LLM 调用的抽象
对标 Spring AI 的 ChatModel:Provider 只管「底层怎么发这一个请求」,
ChatModel 管「这一次逻辑调用」——选项解析、响应归一化、重试,都在这里。
分工的判据是「换一家 provider 要不要重写」:wire 格式要,退避策略不要。
ILLMProvider 一次 HTTP 往返,认识厂商 wire 格式
↑
ChatModel 一次逻辑调用:选项合并 → 重试 → 响应归一化
↑
ChatClient filter 洋葱链 + 工具
↑
SimpleAgent 工具循环 + 记忆 + HITL
重试在这里,位于整个 filter 栈之下(与 beamai beamai_chat_model 同一
取舍,理由见 im.ttalk.agent.retry 的 ns 文档):filter 看到的是「一次逻辑
调用」,重试重入碰不到任何 filter,memory/计时的副作用每轮只发生一次。
call 重试、stream 只在首个 token 之前重试——token 已经投递给 sink 后重跑会让下游看到
重复内容;流式要容错请在 turn 层重跑整轮。
两个实现:
DefaultChatModel:包一个 ILLMProvider,日常用的就是它FnChatModel:包 {:chat-fn :stream-fn} 两个闭包——历史写法与测试 stub
照旧可用(as-chat-model 在装配期归一化),也是接入任意 LLM 的最短路径使用示例:
(require '[im.ttalk.agent.chat-model :as chat-model])
(def cm (chat-model/create-chat-model my-provider {:model "gpt-4" :max-tokens 4096 :retry {:max-retries 3}})) ;; 缺省 2 次;:retry false 关闭
(chat-model/call cm (req/chat-request messages {:tools [...]}))
ChatModel —— 一次 LLM 调用的抽象
对标 Spring AI 的 `ChatModel`:**Provider 只管「底层怎么发这一个请求」,
ChatModel 管「这一次逻辑调用」**——选项解析、响应归一化、重试,都在这里。
分工的判据是「换一家 provider 要不要重写」:wire 格式要,退避策略不要。
ILLMProvider 一次 HTTP 往返,认识厂商 wire 格式
↑
ChatModel 一次逻辑调用:选项合并 → 重试 → 响应归一化
↑
ChatClient filter 洋葱链 + 工具
↑
SimpleAgent 工具循环 + 记忆 + HITL
**重试在这里,位于整个 filter 栈之下**(与 beamai `beamai_chat_model` 同一
取舍,理由见 `im.ttalk.agent.retry` 的 ns 文档):filter 看到的是「一次逻辑
调用」,重试重入碰不到任何 filter,memory/计时的副作用每轮只发生一次。
**`call` 重试、`stream` 只在首个 token 之前重试**——token 已经投递给 sink 后重跑会让下游看到
重复内容;流式要容错请在 turn 层重跑整轮。
两个实现:
- `DefaultChatModel`:包一个 ILLMProvider,日常用的就是它
- `FnChatModel`:包 `{:chat-fn :stream-fn}` 两个闭包——历史写法与测试 stub
照旧可用(`as-chat-model` 在装配期归一化),也是接入任意 LLM 的最短路径
使用示例:
(require '[im.ttalk.agent.chat-model :as chat-model])
(def cm (chat-model/create-chat-model my-provider
{:model "gpt-4" :max-tokens 4096
:retry {:max-retries 3}})) ;; 缺省 2 次;:retry false 关闭
(chat-model/call cm (req/chat-request messages {:tools [...]}))(as-chat-model x)归一化成 ChatModel:已实现 IChatModel 则原样返回,{:chat-fn :stream-fn}
map 包成 FnChatModel。
build-chat-client 对 :chat-model 调用本函数——所以用户侧传 record、
传裸 map、传自定义 IChatModel 实现三种形态等价(与 filter/as-filter
同一套手法)。
归一化成 ChatModel:已实现 IChatModel 则原样返回,`{:chat-fn :stream-fn}`
map 包成 `FnChatModel`。
`build-chat-client` 对 `:chat-model` 调用本函数——所以用户侧传 record、
传裸 map、传自定义 IChatModel 实现三种形态等价(与 `filter/as-filter`
同一套手法)。(call-async* chat-model request)任何 ChatModel 的异步调用入口 → deferred<ChatResponse>。
实现了 IAsyncChatModel 就用原生异步;否则 async/vthread 包 call。
两条路径的语义(重试、归一化、异常)逐字相同。
任何 ChatModel 的异步调用入口 → deferred<ChatResponse>。 实现了 `IAsyncChatModel` 就用原生异步;否则 `async/vthread` 包 `call`。 两条路径的语义(重试、归一化、异常)逐字相同。
(chat-model? x)是否已是 ChatModel(实现 IChatModel 的任意类型)。
是否已是 ChatModel(实现 IChatModel 的任意类型)。
(create-chat-model provider config)从 ILLMProvider 创建 ChatModel。
参数:
返回:DefaultChatModel(实现 IChatModel)
call 同步调用,内建重试(判据为 canonical error 的 :retryable?)stream-call 流式调用,只在首个 token 前重试(token 已投递后重跑即重复)说明:历史的 :build-result-msgs 已移除——对话历史由 filter/memory 的 response->neutral 用中立消息统一构建,ChatModel 无需再提供该函数。
从 ILLMProvider 创建 ChatModel。
参数:
- provider: ILLMProvider 实例
- config: 模型配置 {:model "..." :max-tokens n :system-prompt "..."}
另可带 :retry —— nil/true 用默认(2 次),false 关闭,
map 作为配置({:max-retries 3 :base-delay 1000 ...})
返回:`DefaultChatModel`(实现 IChatModel)
- `call` 同步调用,**内建重试**(判据为 canonical error 的 :retryable?)
- `stream-call` 流式调用,只在首个 token 前重试(token 已投递后重跑即重复)
说明:历史的 :build-result-msgs 已移除——对话历史由 filter/memory 的
response->neutral 用中立消息统一构建,ChatModel 无需再提供该函数。可选协议:ChatModel 能异步调用时实现它。语义与 IChatModel 逐字相同
(含重试策略:call-async 重试、stream-call-async 只在首个 token 前重试),只是不阻塞调用线程。
不实现也能异步:call-async* / stream-call-async* 探测不到会用虚拟线程包
同步调用兜底——调用方永远拿得到 deferred。实现本协议只省掉那根线程。
不得 extend-type Object 兜底(satisfies? 会恒真,探测机制当场失效)。
**可选**协议:ChatModel 能异步调用时实现它。语义与 `IChatModel` 逐字相同 (含重试策略:`call-async` 重试、`stream-call-async` 只在首个 token 前重试),只是不阻塞调用线程。 **不实现也能异步**:`call-async*` / `stream-call-async*` 探测不到会用虚拟线程包 同步调用兜底——调用方永远拿得到 deferred。实现本协议只省掉那根线程。 **不得 `extend-type Object` 兜底**(`satisfies?` 会恒真,探测机制当场失效)。
(call-async this request)-> deferred<ChatResponse>。语义同 call(含重试)。
-> deferred<ChatResponse>。语义同 `call`(含重试)。
(stream-call-async this request on-token)-> deferred<ChatResponse>。语义同 stream-call(只在首个 token 前重试)。
on-token 的调用线程不保证——原生异步实现里 token 在传输层 executor 上派发。
-> deferred<ChatResponse>。语义同 `stream-call`(**只在首个 token 前重试**)。 on-token 的调用线程不保证——原生异步实现里 token 在传输层 executor 上派发。
一次 LLM 调用的契约。对标 Spring AI 的 ChatModel 接口。
call 内建重试;stream-call 只在首个 token 之前重试(token 已出门之后
重跑即重复投递——这条是流式语义的硬约束,designs/provider-reliability.md §2)。
一次 LLM 调用的契约。对标 Spring AI 的 `ChatModel` 接口。 `call` 内建重试;`stream-call` 只在**首个 token 之前**重试(token 已出门之后 重跑即重复投递——这条是流式语义的硬约束,designs/provider-reliability.md §2)。
(call this request)同步调用。
参数: request —— ChatRequest(或可归一化为它的 map)
返回: ChatResponse(实现 ILLMResponse)
抛出: ex-info,data 即 canonical error(重试耗尽后原样重抛)
同步调用。 参数: request —— `ChatRequest`(或可归一化为它的 map) 返回: `ChatResponse`(实现 ILLMResponse) 抛出: ex-info,data 即 canonical error(重试耗尽后原样重抛)
(model-options this)该 ChatModel 的缺省调用选项({:model ... :max-tokens ...})。
单次请求的 :options 覆盖它。
该 ChatModel 的缺省调用选项({:model ... :max-tokens ...})。
单次请求的 `:options` 覆盖它。(stream-call this request on-token)流式调用。on-token 收 {:token ...} / {:reasoning-token ...} 增量。
返回: 流结束时的最终 ChatResponse(与 call 同形)。
provider 不支持流式时回退同步,并把全文作为单个 token emit——保证
chat-stream 对任何 provider 都可用。
只在首个 token 之前重试:见本 ns 文档。
流式调用。on-token 收 {:token ...} / {:reasoning-token ...} 增量。
返回: 流结束时的最终 `ChatResponse`(与 `call` 同形)。
provider 不支持流式时回退同步,并把全文作为单个 token emit——保证
chat-stream 对任何 provider 都可用。
**只在首个 token 之前重试**:见本 ns 文档。(stream-call-async* chat-model request on-token)call-async* 的流式版 → deferred<ChatResponse>。只在首个 token 前重试(同 stream-call)。
⚠️ on-token 的调用线程不保证:原生异步路径上 token 在传输层 executor 上派发, 虚拟线程兜底路径上则在那根虚拟线程上。sink 自行保证线程安全。
`call-async*` 的流式版 → deferred<ChatResponse>。**只在首个 token 前重试**(同 `stream-call`)。 ⚠️ on-token 的调用线程不保证:原生异步路径上 token 在传输层 executor 上派发, 虚拟线程兜底路径上则在那根虚拟线程上。sink 自行保证线程安全。
(with-admission chat-model pool)(with-admission chat-model pool {:keys [key key-fn]})给 ChatModel 套上在途票据池(im.ttalk.agent.admission)。
chat-model 是 DefaultChatModel → 返回同一种 DefaultChatModel,config 挂 :admission:
每次尝试(流式为整条流)取一张票、尝试结束即还,退避等待不占票。AdmittingChatModel 装饰器,
票覆盖整次调用(它看不见内层的重试)。opts:
给 ChatModel 套上在途票据池(`im.ttalk.agent.admission`)。
- `chat-model` 是 DefaultChatModel → 返回同一种 DefaultChatModel,config 挂 `:admission`:
**每次尝试**(流式为整条流)取一张票、尝试结束即还,**退避等待不占票**。
- 其它 ChatModel(FnChatModel / FallbackChatModel / 自定义)→ `AdmittingChatModel` 装饰器,
票覆盖**整次调用**(它看不见内层的重试)。
opts:
- :key-fn (fn [request] key) 按请求算池的键;缺省 DefaultChatModel 为 "<provider>/<model>",
其它为 "default"
- :key 固定键(与 :key-fn 二选一)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 |