状态:✅ 已完成并合入 main(2026-07-10 核对:P0-P7 全部落地,见 §5 与「实施结果」)。 目标:把"显式 thread Context"的状态模型,替换为 Spring AI 式的"无状态调用 + 按 conversation-id 的 Memory store(经 Memory Filter 自管)",并消除消息的双重记账。
落地核对(2026-07-10):
context.clj已是扁平 ToolContext(无 :messages/:history/:trace);advisor/memory.clj以 around-chat filter 自管历史;react.clj的 run-tool-loop 以 delta+store 驱动,无 conv-msgs 影子账——store 是唯一对话来源。路径对照(本文写作期的规划命名 → 实际落地命名):
core/llm/message.clj→model/message.clj;llm/wire/*→provider/wire/*;core/memory.clj→memory.clj;core/kernel/memory_filter.clj→advisor/memory.clj;core/kernel/context.clj→context.clj;simpleagent/*→client.clj(后续 unified-invoke-agent 合并)。
当前架构(重构前):
kernel/invoke 内部跑工具循环,循环局部变量 conv-msgs 是真正发给 LLM 的消息源。Context(嵌套 map::variables/:messages/:history/:trace/:metadata)被显式 thread:调用方传入 :context、拿回 :context、再传入下一轮。ctx.messages 与 conv-msgs 并行记账,且工具结果有两套编码(encode-tool-result vs 各 provider 的 build-result-messages)。tool_use/tool_result content block;OpenAI tool_calls/role:"tool")——换 provider 回放会损坏。问题:
Context.messages 在单次 invoke 内与 conv-msgs 重复,是冗余/可能漂移的影子账。参照 Spring AI 三根支柱:
{:role :system :content "..."}
{:role :user :content "..."}
{:role :assistant :content "..."} ; 纯文本
{:role :assistant :content nil
:tool-calls [{:id "c1" :name "get_weather" :args {:city "北京"}}]} ; 带工具调用
{:role :tool :tool-call-id "c1" :name "get_weather" :content "晴 22°C"}
约束:
:system/:user/:assistant/:tool)。:tool-calls [{:id :name :args}],:name 为字符串或 keyword,:args 为 map。:tool-call-id 关联到对应调用的 :id。每个 provider 负责:
neutral->wire:把中立消息列表转成自家请求格式(call-llm 内部完成)。response->neutral:把原始响应转成中立 assistant 消息(含 :tool-calls)。provider 协议调整:
call-llm [this config neutral-messages tools] —— 内部 neutral→wire。response->neutral-message [this response] —— 取代 build-assistant-message,返回中立 assistant。build-result-messages / build-tool-result:工具结果消息由 run-tools / Memory Filter 直接构造中立形状,provider 无关。extract-text / extract-tool-calls 保留(response→neutral 内部复用)。(defprotocol ChatMemory
(mem-get [this conv-id]) ; -> [中立消息 ...]
(mem-add [this conv-id msgs]) ; 追加中立消息
(mem-clear [this conv-id]))
{conv-id -> [msgs]})。(windowed store {:max-messages N}) 或 token 预算(后续)。;; pre-chat:存 delta、用完整历史替换 messages
(create-filter :memory :pre-chat
(fn [fctx]
(let [cid (get-in fctx [:context :conversation-id])
delta (:messages fctx)]
(if (and store cid)
(do (mem-add store cid delta)
{:action :continue
:context (assoc-messages fctx (mem-get store cid))})
{:action :continue :context fctx})))) ; 无记忆 → 原样
;; post-chat:存 assistant(可能含 tool-calls)
(create-filter :memory :post-chat
(fn [fctx]
(when (and store cid)
(mem-add store cid [(response->neutral (:response fctx))]))
{:action :continue :context fctx}))
存储顺序天然正确:[..., user, assistant(tool_calls), tool_result(s), assistant(...)]。
单层 flat map,承载请求/会话级 k/v:
{:conversation-id "abc" :user-id "u1" :任意工具共享 k v}
:messages/:history/:trace。{:context true} 工具读写它(请求级,返回但不自动持久化)。get-var/set-var 退化为普通 map 操作。invoke kernel [user-msg] {:tool-context {:conversation-id cid}}:
1. invoke-chat [user-msg] ; 只传 delta
pre-chat 存 user + 拼历史 → LLM → post-chat 存 assistant
2. while has-tool-calls(response):
results = run-tools kernel tool-calls tool-context ; 中立 tool 消息
invoke-chat results ; 只传 delta
... 同上 ...
3. return {:response ... :tool-context tc}
conv-msgs、删除 threaded ctx.messages:store 是唯一对话来源。invoke-chat(单步、不执行工具)+ 新公开 run-tools(批量执行 → 中立 tool 消息)。| 阶段 | 内容 | 状态 |
|---|---|---|
| P0 | 中立消息 spec + 转换契约(本文档 §3.1)→ core/llm/message.clj | ✅ |
| P1 | provider 边界 neutral↔wire → llm/wire/{openai,anthropic}.clj + 单测(7 tests) | ✅ wire 单元完成;service.chat-fn 接入留待 P4 整合 |
| P2 | ChatMemory protocol + in-memory + 窗口策略 → core/memory.clj + 单测 | ✅ |
| P3 | Memory Filter(pre/post-chat)→ core/kernel/memory_filter.clj + 单测 | ✅ |
| P4 | 改 invoke:delta + store;Context→flat ToolContext;{:context true} 工具改吃 flat map;kernel 内建 store + 自动挂 Memory Filter;process runtime 改扁平合并 | ✅ |
| P5 | 暴露 run-tools(public)+ 外部手搓循环测试(external-tool-loop-test) | ✅ |
| P6 | 重写 SimpleAgent:kernel-agent / process-agent / common 全部改 conversation-id + store | ✅ |
| P7 | 重写 context_test;修 kernel_agent_test/process_agent_test;修 examples(integration/kernel/process_test) | ✅ |
refactor/memory-filter-neutral-messages(未提交,待 review)。core/llm/message.clj:中立消息(构造/谓词/normalize)llm/wire/{openai,anthropic}.clj + llm/kernel/chat.clj:neutral↔wire,service.chat-fn 吃中立消息core/memory.clj:ChatMemory(in-memory + pairing-safe 窗口)core/kernel/memory_filter.clj:pre/post-chat 自管历史core/kernel.clj:invoke 走 delta+store(无 conv-msgs/双账)、run-tools 公开、kernel 内建 storecore/kernel/context.clj:扁平 ToolContextsimpleagent/*:conversation-id + store(去 context-atom)core/kernel/context.clj:移除 track-message/get-messages/get-history/双轨,只剩 flat-map 的 get-var/set-var。invoke/invoke-chat::context → :tool-context;返回 :tool-context。call-llm 吃中立消息;新增 response->neutral-message;移除 build-result-messages/build-tool-result/build-assistant-message。create-agent/chat/reset!/get-history 语义变化(context-atom → conversation-id + store)。P1(provider neutral↔wire)是地基与工作量主体。必须先打通"中立 ↔ Anthropic / OpenAI wire"并单测覆盖(mock + schema 比对),再往上叠 store/filter/invoke。
Can you improve this documentation?Edit on GitHub
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 |