- 人工智能
- 大模型
- AI Agent
- RAG
- 工具调用
- MCP 服务
- MCP Clients
- 多智能体
【免费下载链接】CangjieMagic
基于仓颉编程语言构建的 LLM Agent DSL,其主要特点包括:声明式 DSL、支持 MCP 协议,支持任务智能规划等。
CangjieMagic 是一个基于仓颉编程语言构建的 LLM Agent 框架,其 memory 包 提供了以语义相似度为核心的短期记忆能力。本指南围绕 docs/package_docs/memory.md 定义的ShortMemory类,从接口契约、内部实现、Agent 集成到实战配置逐层展开,帮助你掌握在 CangjieMagic 中为 Agent 启用记忆、检索历史对话并定制记忆策略的完整方法。
读完本文你将能够:理解Memory接口的语义与ShortMemory的向量检索原理;通过@agent宏的memory属性或编程方式为 Agent 注入记忆;知道当前记忆的 in-memory 特性与局限,并具备扩展自定义记忆实现的思路。
从抽象接口到具体实现:记忆包的总体结构
CangjieMagic 的记忆能力分为两层:契约层与实现层。
契约层定义在 src/core/memory/memory.cj,属于magic.core.memory包。它声明了最小且稳定的记忆抽象——Memory接口,任何记忆实现都必须提供两个方法:
public interface Memory { /** * Update the memory */ func update(segment: String): Unit /** * According to the user question, find related content in the memory */ func search(question: String): Array<String> }这两个方法恰好对应memory包文档(docs/package_docs/memory.md)中列出的全部公开 API:
| 方法 | 签名 | 语义 |
|---|---|---|
search | func search(question: String): Array<String> | 根据用户问题,在记忆中查找相关内容,返回匹配的文本片段数组 |
update | func update(segment: String): Unit | 向记忆中追加一段文本 |
而实现层则位于 src/memory/short_memory.cj,属于magic.memory包。ShortMemory是Memory接口的唯一内置实现,通过内存中的向量数据库完成语义化存储与检索。
public class ShortMemory <: Memory { private let vecSet = SemanticSet(vectorDB: InMemoryVectorDatabase(), indexMap: SimpleIndexMap(), embeddingModel: Config.defaultEmbeddingModel) override public func update(segment: String): Unit { vecSet.put(segment) } override public func search(question: String): Array<String> { return vecSet.search(question, number: 5) } }从源码可以清晰看出ShortMemory的三个关键决策:
- 语义检索而非关键词匹配:存储与查询都依赖向量数据库与嵌入模型,按语义相似度召回,而非简单的字符串包含;
- 纯内存、非持久化:
InMemoryVectorDatabase即“内存向量数据库”,进程退出后数据即丢失,与教程文档中“目前记忆仅支持 in-memory 非持久化数据”(见 docs/tutorial.md)的说明一致; - 固定召回数量:
search固定返回语义最相近的 5 条文本。
值得一提的是,ShortMemory并不依赖框架的全局配置项在运行时查找嵌入模型,而是在类初始化时直接绑定Config.defaultEmbeddingModel(见 src/config/config.cj 中defaultEmbeddingModel的声明)。这意味着在使用记忆前,必须先通过ModelManager配置好默认嵌入模型,否则ShortMemory内部的向量构建环节会因缺少模型而失败。
ShortMemory 底层原理:SemanticSet 与向量检索链路
ShortMemory只有寥寥十几行,其核心能力全部委托给了SemanticSet。理解SemanticSet及其背后的SemanticMap、InMemoryVectorDatabase、SimpleIndexMap,才能真正掌握记忆的工作机理。
SemanticSet:键值一体的语义集合
src/rag/vdb/semantic_set.cj 中对SemanticSet的定位是一句话:“SemanticSet 是一种 key 与 value 相同的特殊 SemanticMap”。它内部持有一个SemanticMap,对外暴露三个方法:
public func put(value: T): Unit { semanticMap.put(value.toString(), value) } public func search(query: String, number!: Int64 = 5, minDistance!: Float64 = 0.3): Array<T> { return semanticMap.search(query, number: number, minDistance: minDistance) }put:把文本片段本身同时当作 key 和 value 写入,这正是“记忆”的本质——记忆体里存的就是原始文本;search:查询时用问题文本生成向量,在集合中按相似度召回,默认召回 5 条、最小相似度阈值为0.3。
SemanticMap:向量化存储与索引的中枢
src/rag/vdb/semantic_map.cj 中SemanticMap把三个组件串联成完整链路:嵌入模型生成向量 → 向量数据库做相似度排序 → 索引映射回原始文本。
写入路径put(key, value):
public func put(key: String, value: T): Unit { let keyVec = this.vecBuilder.createEmbeddingVector(key) vectorDB.addVector(keyVec) indexMap.add(value) }查询路径search(query, ...):
public func search(query: String, number!: Int64 = 5, minDistance!: Float64 = 0.3): Array<T> { let queryVec = this.vecBuilder.createEmbeddingVector(query) vectorDB.search(queryVec, number: number, minDistance: minDistance) |> map { sr: SearchResult => indexMap.get(sr.index) } |> collectArray }查询时先对问题文本调用VectorBuilder.createEmbeddingVector生成查询向量,交给向量数据库排序,再把命中的向量下标通过索引映射回原始文本片段返回。其中VectorBuilder(见 src/rag/vdb/vector_builder.cj)封装了对EmbeddingModel.create(EmbeddingRequest(content))的调用。
InMemoryVectorDatabase:基于余弦相似度的排序与截断
src/rag/vdb/in_memory_vector_database.cj 是记忆的“存储引擎”,其search逻辑值得细看:
public override func search(queryVec: Vector, number!: Int64 = 5, minDistance!: Float64 = 0.6): Array<SearchResult> { if (vectorBuffer.isEmpty()) { LogUtils.info("Vector buffer is empty") return [] } let tempList = vectorBuffer.clone() tempList.sortBy(stable: true) { a: VectorWithIndex, b: VectorWithIndex => if (a.vector.cosineSimilarity(queryVec) > b.vector.cosineSimilarity(queryVec)) { return Ordering.LT } ... } ... }关键行为有三点:
- 相似度指标为余弦相似度(
cosineSimilarity),值越大表示与查询问题语义越接近,排序后取前number个; - 双重截断:既限制返回条数,又设
minDistance相似度下限,低于阈值的命中会直接break丢弃,避免召回无关内容; - 空库保护:向量库为空时直接返回空数组并输出一条 info 日志,而不是报错。
注意SemanticSet、SemanticMap与InMemoryVectorDatabase的默认minDistance分别为0.3、0.3、0.6,说明记忆检索的相似度门槛由上层决定,最终以向量库阈值为准。
SimpleIndexMap:下标到文本的映射
src/rag/vdb/simple_index_map.cj 用HashMap<String, String>维护“向量下标 → 文本内容”的映射:add(content)按当前map.size分配下标,get(index)按下标取回文本。该实现实现了序列化接口并支持save/load到 JSON 文件,但ShortMemory当前并未启用持久化——向量库的save/load在InMemoryVectorDatabase中直接抛出UnsupportedException(见 src/rag/vdb/in_memory_vector_database.cj),这也再次印证了短期记忆“非持久化”的定位。
记忆在 Agent 中的生命周期:谁来写入、何时写入
ShortMemory自身只提供update/search两个方法,真正把它接入 Agent 运行流程的是 Agent 基类。在 src/agent/base/abs_agent.cj 的chat方法中可以看到写入时机:
let resp = this.executor.run(this, processedRequest) match (resp.status) { case AgentResponseStatus.Success => // Update the agent memory if needed if (let Some(memory) <- this.memory) { memory.update(resp.execution.chatRound.toString()) } case _ => () }写入时机有两个明确特征:
- 仅在执行成功时写入:
AgentResponseStatus.Success分支之外(包括取消、失败)不会更新记忆; - 写入内容为整轮对话:
resp.execution.chatRound.toString()将一轮完整的问答(用户问题与助手回复)序列化为文本片段后写入记忆。
读取时机则由各执行器在构造 prompt 时调用memory.search。例如 src/agent_executor/common/prompt_builder.cj、src/agent_executor/react/react_prompt_utils.cj 等文件都引用了Memory——执行器在组装系统提示时会从记忆中检索与当前问题语义相近的历史片段,注入 prompt,让 Agent 具备“跨轮次回忆起之前聊过什么”的能力。BaseAgent的构造函数也以memory!: Option<Memory> = None的形式接受外部记忆实现(见 docs/tutorial.md 与 docs/package_docs/agent.base.md),默认不启用。
快速上手:为 Agent 开启记忆的两种方式
方式一:@agent 宏声明式开启(推荐)
CangjieMagic 的 DSL 层把记忆封装成了@agent宏的一个布尔属性。在 docs/package_docs/dsl.md 的属性表中:
| 属性 | 类型 | 说明 |
|---|---|---|
memory | Bool | 是否启用短期记忆,默认关闭 |
用法示例(取自 docs/how-to.md 的配置清单):
@agent[ description: "A helpful assistant for weather information", model: "deepseek:deepseek-chat", temperature: 0.7, memory: true, executor: "react" ] class WeatherAgent { }宏展开阶段,src/dsl/agent.cj 的buildAgentMemoryProp会依据memory属性生成记忆属性:
private func buildAgentMemoryProp(agentAttr: AgentAttr): Tokens { let tokens = if (agentAttr.memory) { quote(Some(ShortMemory())) } else { quote(None) } ... }也就是说,memory: true会在生成的 Agent 类中注入一个惰性初始化的memory: Option<Memory>属性,首次访问时才构造ShortMemory()实例。这也是绝大多数场景下推荐的使用方式:一行属性即完成记忆的装配。
方式二:编程式注入自定义记忆
如果不想依赖 DSL,也可以直接用BaseAgent的构造函数注入记忆实现:
import magic.core.memory.* import magic.memory.ShortMemory let agent = BaseAgent( model: ModelManager.createChatModel("ollama:phi3"), executor: AgentExecutorManager.createExecutor("react"), memory: Some(ShortMemory()) )BaseAgent的完整构造签名(见 docs/tutorial.md)为:
class BaseAgent <: Agent { public init( name!: String = "Base Agent", description!: String = "", temperature!: Option<Float64> = None, systemPrompt!: String = "", toolManager!: ToolManager = SimpleToolManager(), model!: Option<ChatModel> = None, executor!: Option<AgentExecutor> = None, retriever!: Option<Retriever> = None, memory!: Option<Memory> = None, interceptor!: Option<Interceptor> = None ) }memory参数类型是Option<Memory>而非具体的ShortMemory,这意味着你可以传入任何实现了Memory接口的自定义类型——这为扩展持久化记忆、数据库记忆等留出了清晰的扩展点。
前提与限制:使用记忆前必须配置嵌入模型
由于ShortMemory在构造时直接读取Config.defaultEmbeddingModel(src/config/config.cj),且SemanticMap在缺少嵌入模型时会抛出异常(getOrThrow({ => Exception("Embedding model is not set") }),见 src/rag/vdb/semantic_map.cj),因此启用记忆前必须先完成默认嵌入模型的设置。
以 OpenA 兼容的嵌入模型为例(可通过ModelManager按需配置):
import magic.model.ModelManager // 使用 ModelManager 创建并设置为默认嵌入模型 ModelManager.defaultEmbeddingModel = ModelManager.createEmbeddingModel("ollama:nomic-embed-text")当前框架内置了多种嵌入模型适配器,包括 ollama、dashscope、llamacpp、openai 等,可在ModelManager中按提供商前缀(如ollama:、dashscope:、openai:)创建对应实例。
需要牢记的当前版本限制:
- 仅限内存:
ShortMemory基于InMemoryVectorDatabase,进程重启后记忆全部丢失,不适用于需要长期跨会话记忆的场景; - 固定召回条数:
search固定返回 5 条,若业务上需要更多上下文,可参考SemanticSet.search的number参数自行实现记忆类; - 依赖默认嵌入模型:必须先设置
Config.defaultEmbeddingModel,否则运行期构造ShortMemory或首次检索时会抛出异常。
扩展思路:如何定制自己的记忆实现
如果你需要持久化记忆、更大的召回量或不同的相似度阈值,最自然的做法是实现自己的Memory子类,复用框架已有的向量组件,例如把SemanticSet换成可持久化的组合:
public class PersistentMemory <: Memory { // 可替换为 Faiss 等支持 save/load 的向量库组合 // 例如 SemanticSet(vectorDB: FaissVectorDatabase(), indexMap: JsonlIndexMap(), ...) public override func update(segment: String): Unit { // 持久化写入 } public override func search(question: String): Array<String> { // 自定义召回数量与阈值 } }框架在 src/rag/vdb 目录下还提供了FaissVectorDatabase、JsonlIndexMap、SimpleIndexMap等多个可替换组件(见 src/rag/vdb/faiss_vector_database.cj、src/rag/vdb/jsonl_index_map.cj),它们支持save/load持久化,可作为构建长期记忆的基础设施。之后通过memory: Some(PersistentMemory())或 DSL 生成的memory属性 setter 注入即可。
总结
CangjieMagic 的记忆能力虽然抽象简洁——Memory接口仅两个方法、ShortMemory仅一个类——但背后是一条完整的语义检索链路:Embedding 模型向量化 →SemanticSet/SemanticMap组织存储 →InMemoryVectorDatabase余弦相似度排序 →SimpleIndexMap还原文本。在使用层面,@agent宏的memory: true即可一键启用,执行成功后整轮对话会被写入记忆,下一次提问时语义相近的历史片段会被检索注入 prompt。当前实现为纯内存非持久化,适合会话内的跨轮上下文增强;对于长期记忆需求,Memory接口与SemanticSet等向量组件的可组合性为你预留了清晰的定制路径。
- 人工智能
- 大模型
- AI Agent
- RAG
- 工具调用
- MCP 服务
- MCP Clients
- 多智能体
【免费下载链接】CangjieMagic
基于仓颉编程语言构建的 LLM Agent DSL,其主要特点包括:声明式 DSL、支持 MCP 协议,支持任务智能规划等。
相关推荐
OpenClaw `clawdbot memory` 命令完全指南:语义记忆索引与检索实战
OpenClaw clawdbot memory 命令完全指南:语义记忆索引与检索实战 导读 clawdbot memory 是 OpenClaw 中管理 语义
人工智能AI Agent即时通讯后端本地部署语音Operit 记忆管理 API 完全指南:掌握 `Tools.Memory` 实现语义检索、快照去重与记忆链接
Operit 记忆管理 API 完全指南:掌握 Tools.Memory 实现语义检索、快照去重与记忆链接 本文是 Operit 工具包开发中 Tools.Me
AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆GUI 自动化OpenClaw memory 命令实战:status / index / search 语义记忆索引与检索指南
OpenClaw memory 命令实战:status / index / search 语义记忆索引与检索指南 openclaw memory 是 OpenC
人工智能大模型LLM 网关AI Agent即时通讯
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考