AI SDK Agent 记忆功能怎么实现:provider 工具、memory provider 与自定义工具怎么选
2026/9/13 12:23:29 网站建设 项目流程

AI SDK Agent 记忆功能怎么实现:provider 工具、memory provider 与自定义工具怎么选

【免费下载链接】aiThe AI Toolkit for TypeScript. From the creators of Next.js, the AI SDK is a free open-source library for building AI-powered applications and agents项目地址: https://gitcode.com/GitHub_Trending/ai/ai

给 Agent 加上记忆,就是让它在一次对话里存入信息、在之后的对话里再取回来。没有记忆时,每次对话都从零开始;有了记忆,Agent 可以逐步积累上下文、回忆之前的交互并适应用户。AI SDK 官方文档 Memory 给出了三条实现路径:provider-defined tools、memory providers、自定义工具。本文基于该文档及其引用的各 provider 文档、Build a Custom Memory Tool 配方,梳理每条路径的实际写法和适用条件,最后给出一份可对照的选择依据。

三种方案的工作方式与代价对比

官方文档的对比表如下,这是选择方案的总依据:

方案实现工作量灵活性Provider 锁定
Provider-Defined Tools
Memory Providers取决于所用 memory provider
Custom Tool

三者的本质区别:

  • Provider-Defined Tools:工具本身(inputSchemadescription)由模型厂商定义,模型已经针对这些工具做过训练,你只提供execute函数,把工具调用映射到自己的存储后端。文档指出这相比自定义工具可能带来更好的表现,代价是与特定厂商绑定。
  • Memory Providers:外部记忆服务通过标准 AI SDK 接口接入,记忆的存储、检索、注入全部在 provider 侧透明完成,你不需要自己定义任何工具。代价是记忆行为由 provider 控制,你对"存了什么、怎么取回"可见度更低,并且依赖一个外部服务。
  • Custom Tool:自己定义工具接口、存储格式和检索逻辑。前期工作量最大,但完全自主,无 provider 锁定、无外部依赖。

方案一:Provider-Defined Tools(以 Anthropic Memory Tool 为例)

Anthropic 的 Memory Tool 给 Claude 一个管理/memories目录的结构化接口:Claude 在任务开始前读取自己的记忆,工作时创建和更新文件,并在后续对话中引用它们。前提是你已经在用 Anthropic 模型——文档明确说明该工具只在 Claude 上可用。

工具接收结构化命令(viewcreatestr_replaceinsertdeleterename),每条命令带一个限定在/memories下的path。你的execute函数负责把这些命令映射到存储后端(文件系统、数据库或其他持久层),并把结果作为字符串返回:

import { anthropic } from '@ai-sdk/anthropic'; import { ToolLoopAgent } from 'ai'; const memory = anthropic.tools.memory_20250818({ execute: async action => { // `action` contains `command`, `path`, and other fields // depending on the command (view, create, str_replace, // insert, delete, rename). // Implement your storage backend here. // Return the result as a string. }, }); const agent = new ToolLoopAgent({ model: 'anthropic/claude-haiku-4.5', tools: { memory }, }); const result = await agent.generate({ prompt: 'Remember that my favorite editor is Neovim', });

这段代码来自 Memory 文档。execute函数体是留给你实现的存储逻辑,模型、包名和工具名按原文保留即可直接使用。

如何验证:记忆写入是否生效取决于你的execute实现。以文件系统为例,发一条"记住我的编辑器是 Neovim"之类的 prompt 后,检查存储后端中是否出现对应记忆文件(如/memories下的文件)及其内容;在后续对话里再提问,确认模型能通过view等命令读回该信息。文档中"下次运行时该事实出现在 system prompt / 对话中"这类行为描述,需要你在自己的存储实现上实际跑一遍才能确认。

适用判断:文档给出的条件是"想以最小实现工作量获得记忆,且已在用 Anthropic 模型"。如果你同时需要跨多家模型供应商,这条路径直接出局。

方案二:Memory Providers(Letta、Mem0、Supermemory、Hindsight)

这一类 provider 把外部记忆服务包装成标准接口。官方 Memory 文档列出了四个代表:Letta、Mem0、Supermemory、Hindsight,分别覆盖"托管 Agent 运行时记忆"、"通用记忆层"、"语义搜索自动存取"、"可自托管的五工具记忆库"。下面按"最小接入 + 验证"的粒度过一遍。

Letta:记忆由 Letta agent 运行时托管

Letta 提供持久长期记忆。你在 Letta 平台(cloud 或 self-hosted)创建 Agent 并在那里配置其记忆,AI SDK 的 provider 负责与它交互,记忆管理(core memory、archival memory、recall)由 Letta 的 agent 运行时处理。

安装:

pnpm add @letta-ai/vercel-ai-sdk-provider

.env中配置 API key(从 Letta dashboard 获取):

# .env LETTA_API_KEY=your-letta-api-key

your-letta-api-key替换为你自己的 Letta API key。最简用法:

import { lettaCloud } from '@letta-ai/vercel-ai-sdk-provider'; import { ToolLoopAgent } from 'ai'; const agent = new ToolLoopAgent({ model: lettaCloud(), providerOptions: { letta: { agent: { id: 'your-agent-id' }, }, }, }); const result = await agent.generate({ prompt: 'Remember that my favorite editor is Neovim', });

'your-agent-id'替换为你在 Letta 平台创建的 Agent 的 ID。注意lettaCloud()只是 provider 实例,模型配置(LLM、temperature 等)由你的 Letta Agent 管理,见 Letta provider 文档。自托管用户导入lettaLocal代替lettaCloud,或用createLetta({ baseUrl, token })创建自定义实例。

如果需要在你的自定义工具之外显式使用 Letta 内置的记忆工具,可以这样声明(这些工具的执行由 Letta 完成):

import { lettaCloud } from '@letta-ai/vercel-ai-sdk-provider'; import { ToolLoopAgent } from 'ai'; const agent = new ToolLoopAgent({ model: lettaCloud(), tools: { core_memory_append: lettaCloud.tool('core_memory_append'), memory_insert: lettaCloud.tool('memory_insert'), memory_replace: lettaCloud.tool('memory_replace'), }, providerOptions: { letta: { agent: { id: 'your-agent-id' }, }, }, }); const stream = agent.stream({ prompt: 'What do you remember about me?', });

如何验证:先让 Agent 记住一条事实,再问"What do you remember about me?"这类问题,让 Letta 侧的回忆能力作答。此外 provider 继承了@letta-ai/letta-client,可以通过lettaCloud.client(如lettaCloud.client.agents.list())直接调用 Letta API 核对 Agent 与记忆配置。

Mem0:加在任意受支持 LLM provider 之上的记忆层

Mem0 自动从对话中提取记忆、存储,并在后续 prompt 中检索相关记忆。安装:

pnpm add @mem0/vercel-ai-provider

初始化 provider 实例。provider指定底层 LLM(文档支持openaianthropicgooglegroqcohere五种配置值),两个 key 建议用环境变量(MEM0_API_KEY从 Mem0 dashboard 获取):

import { createMem0 } from '@mem0/vercel-ai-provider'; import { ToolLoopAgent } from 'ai'; const mem0 = createMem0({ provider: 'openai', mem0ApiKey: process.env.MEM0_API_KEY, apiKey: process.env.OPENAI_API_KEY, }); const agent = new ToolLoopAgent({ model: mem0('gpt-4.1', { user_id: 'user-123' }), }); const { text } = await agent.generate({ prompt: 'Remember that my favorite editor is Neovim', });

user_id用来标识记忆归属,文档的最佳实践是"用唯一的user_id保证记忆检索的一致性",把它替换为你的用户标识。

不经过模型、直接管理记忆的函数:

import { addMemories, retrieveMemories } from '@mem0/vercel-ai-provider'; await addMemories(messages, { user_id: 'user-123' }); const context = await retrieveMemories(prompt, { user_id: 'user-123' });

Mem0 文档还给出了两个细节:getMemories返回原始记忆(对象数组),retrieveMemories返回已注入检索记忆的 system prompt 字符串格式;生成响应时可以解构出sources查看记忆来源:

const { text, sources } = await generateText({ model: mem0('gpt-4.1'), prompt: 'Suggest me a good car to buy!', }); console.log(sources);

如何验证:调用addMemories写入对话后,用getMemories拿到数组确认记忆确实被存入,用retrieveMemories确认返回的上下文中包含该事实;生成回答时检查sources是否命中对应记忆。这些函数单独调用时,MEM0_API_KEY必须通过环境变量或函数参数提供。

Supermemory:语义搜索驱动的自动存取

Supermemory 通过语义搜索自动保存和检索记忆,工具暴露addMemorysearchMemories两个操作,可搭配任意 AI SDK provider 使用。安装:

pnpm add @supermemory/tools
import { generateText } from 'ai'; import { createOpenAI } from '@ai-sdk/openai'; import { supermemoryTools } from '@supermemory/tools/ai-sdk'; const openai = createOpenAI({ apiKey: 'YOUR_OPENAI_KEY', }); const { text } = await generateText({ model: openai('gpt-5-mini'), prompt: 'Remember that my name is Alice', tools: supermemoryTools('YOUR_SUPERMEMORY_KEY'), }); console.log(text);

代码来自 Supermemory 文档。YOUR_OPENAI_KEYYOUR_SUPERMEMORY_KEY是占位符,分别替换为你的 OpenAI API key 和 Supermemory API key(Supermemory 提供免费的 API key)。该文档还展示了另一种可选接法——Memory Router:把 provider 的baseUrl换成 Supermemory 的代理地址并通过x-supermemory-api-keyx-sm-conversation-id头传递身份,不写工具、让路由层处理记忆;它和上面的工具方式是两条并列路径,本文主线采用工具方式。

如何验证:文档示例指出模型会自动调用searchMemories({ informationToGet: ... })addMemory({ memory: ... })。因此验证方式是:发送"记住我叫 Alice"后,再次询问"我叫什么",同时观察工具调用日志里是否出现了addMemory/searchMemories的调用。

Hindsight:五工具记忆库,可 Docker 自托管

Hindsight 通过retainrecallreflectgetMentalModelgetDocument五个工具提供持久记忆,可 Docker 自托管或用作云服务,支持generateTextstreamTextToolLoopAgent

自托管(说明副作用:下面的docker run会启动一个容器,占用本机8888(API)和9999(UI)两个端口,并把持久卷挂载到$HOME/.hindsight-docker,需要 Docker 环境和一个用于其内部 LLM 调用的OPENAI_API_KEY):

export OPENAI_API_KEY=your-key docker run --rm -it -p 8888:8888 -p 9999:9999 \ -e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \ -v $HOME/.hindsight-docker:/home/hindsight/.pg0 \ ghcr.io/vectorize-io/hindsight:latest

your-key替换为你自己的 OpenAI key。启动后 API 在http://localhost:8888,UI 在http://localhost:9999。用云服务则从 Hindsight dashboard 拿到 API URL 写入HINDSIGHT_API_URL环境变量即可。

安装并创建工具:

pnpm add @vectorize-io/hindsight-ai-sdk @vectorize-io/hindsight-client
import { HindsightClient } from '@vectorize-io/hindsight-client'; import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk'; import { generateText, isStepCount } from 'ai'; import { openai } from '@ai-sdk/openai'; const client = new HindsightClient({ baseUrl: process.env.HINDSIGHT_API_URL }); const tools = createHindsightTools({ client, bankId: 'user-123' }); const { text } = await generateText({ model: openai('gpt-4o'), tools, stopWhen: isStepCount(5), system: 'You are a helpful assistant with long-term memory.', prompt: 'Remember that I prefer dark mode and large fonts.', });

bankId标识记忆库,通常是用户 ID。多用户应用里要在请求处理器内调用createHindsightTools,让每个请求用上当前用户的bankId——HindsightClient在模块级创建一次共享,工具按请求创建。基础设施类选项(retainasync/tags/metadatarecallbudget/types/maxTokens等)在创建工具时配置,语义层面的决定(记什么、搜什么)留给模型,详见 Hindsight 文档。

如何验证:自托管场景下先访问http://localhost:9999的 UI 确认服务已起来;然后让 Agent 记住一条偏好,再发一个需要回忆的问题(如"我偏好什么界面设置"),确认回答命中之前存入的事实。

这一类的适用判断直接来自官方文档:当你不想自建存储基础设施时,memory providers 是合适的;代价是 provider 控制记忆行为,可见度更低,且引入外部服务依赖。

方案三:自定义工具(Custom Tool)

如果存储格式、接口和检索逻辑都要自己掌握,就走自定义工具。官方配方 Build a Custom Memory Tool 给出了一条完整可跑的路径:在一个共享的.memory目录上建一个记忆工具,再用prepareCall把核心记忆注入每次模型调用。

该配方定义了三种记忆类型(术语取自 Letta 的定义):

  • Core Memory:每轮都注入的信息,直接写进 system prompt,不需要工具调用;
  • Archival Memory:模型按需通过记忆工具读写的笔记文件;
  • Recall Memory:完整的逐轮对话历史,持久化后可搜索。

记忆目录布局:

.memory/ ├── core.md # Core memory, injected every turn ├── notes.md # Archival memory, timestamped notes └── conversations.jsonl # Recall memory, full turn history (JSONL)

准备依赖

两条路线共用的安装命令:

pnpm add ai just-bash zod

如果只用结构化动作路线(Route A),可以跳过just-bash

存储底座:启动时引导文件系统

一次性初始化:目录不存在就创建,文件不存在就写入初始内容,并把.memory加入.gitignore保持记忆本地私有:

import { access, appendFile, mkdir, readFile, writeFile, } from 'node:fs/promises'; import { join, resolve } from 'node:path'; const MEMORY_DIR = '.memory'; const MEMORY_ROOT = resolve(process.cwd(), MEMORY_DIR); const CORE_MEMORY_PATH = join(MEMORY_ROOT, 'core.md'); const NOTES_PATH = join(MEMORY_ROOT, 'notes.md'); const CONVERSATIONS_PATH = join(MEMORY_ROOT, 'conversations.jsonl'); const DEFAULT_CORE_MEMORY = `# Core Memory - Keep this short. - Put stable user facts here. `; const DEFAULT_NOTES = `# Notes Use this file for detailed memories and timestamped notes. `; async function ensureFile(path: string, content: string): Promise<void> { try { await access(path); } catch { await writeFile(path, content, 'utf8'); } } async function ensureMemoryFilesystem(): Promise<void> { await mkdir(MEMORY_ROOT, { recursive: true }); await ensureFile(CORE_MEMORY_PATH, DEFAULT_CORE_MEMORY); await ensureFile(NOTES_PATH, DEFAULT_NOTES); await ensureFile(CONVERSATIONS_PATH, ''); }

配套的读取与追加辅助函数(读 core 记忆用于注入、把对话追加为 JSONL,方便grepjq处理):

async function readCoreMemory(): Promise<string> { try { return await readFile(CORE_MEMORY_PATH, 'utf8'); } catch { return ''; } } async function appendConversation(entry: { role: 'user' | 'assistant'; content: string; timestamp: string; }): Promise<void> { await appendFile(CONVERSATIONS_PATH, `${JSON.stringify(entry)}\n`, 'utf8'); }

工具接口:两条路线二选一

Route A:结构化动作。定义显式动作(viewcreateupdatesearch),所有请求都经过你自己的 handler,每个操作都受你控制,天然安全,但前期实现更多、模型只能调用你建好的动作:

import { tool } from 'ai'; import { z } from 'zod'; const memoryInputSchema = z.object({ command: z .enum(['view', 'create', 'update', 'search']) .describe( 'Memory action: view to read, create to write new content, update to change existing content, search to find relevant lines.', ), path: z .string() .optional() .describe( 'Memory path under /memories, such as /memories/core.md or /memories/notes.md. Required for view, create, and update.', ), content: z .string() .optional() .describe('Text to write for create or update commands.'), mode: z .enum(['append', 'overwrite']) .optional() .describe( 'Write mode for update: append adds to existing content, overwrite replaces it. Defaults to overwrite.', ), query: z .string() .optional() .describe( 'Search keywords for the search command. Prefer short focused terms.', ), }); const memoryTool = tool({ description: `Use this tool to read and maintain long-term memory under /memories. Rules: - If the user prompt might depend on preferences, history, constraints, or goals, search first, then reply. - If the prompt is fully self-contained or general knowledge, reply directly. - Keep searches short and focused (1-4 words). - Store durable user facts in /memories/core.md and detailed notes in /memories/notes.md. - Keep memory operations invisible in user-facing replies.`, inputSchema: memoryInputSchema, execute: async input => { try { const output = await runMemoryCommand(input); return { output }; } catch (error) { return { output: `Memory action failed: ${(error as Error).message}` }; } }, });

runMemoryCommand的实现要点:把路径解析到 memory 根目录下,只允许core.mdnotes.mdconversations.jsonl这几个已知文件,然后按动作执行——view读文件,create/updateappendoverwrite写入,search按关键词逐行匹配并返回文件:行号:内容形式。完整实现见配方的 Appendix: Structured Actions Handler。

Route B:Bash 支撑。给模型一个沙箱化的 bash 环境去组合catgrepsedecho等命令,灵活性更高,但必须做命令校验防注入。配方用 just-bash(一个 JavaScript 实现的 bash,不启动真实 shell 进程)执行命令,配合 AST 层命令守卫:

import { tool } from 'ai'; import { Bash, ReadWriteFs } from 'just-bash'; import { z } from 'zod'; const fs = new ReadWriteFs({ root: process.cwd() }); const bash = new Bash({ fs, cwd: '/' }); const memoryTool = tool({ description: `Run bash commands only for memory-related tasks. ...`, inputSchema: z.object({ command: z.string().describe('The bash command to execute.'), }), execute: async ({ command }) => { const unapprovedCommand = findUnapprovedCommand(command); if (unapprovedCommand) { return { stdout: '', stderr: `Blocked unapproved command: ${unapprovedCommand}\n`, exitCode: 1, }; } const result = await bash.exec(command); return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, }; }, });

上面的description在配方中是一大段路径规则与示例命令(限定只操作/.memory下的文件、用>>追加、perl -pi -e原地编辑等),这里为紧凑做了省略,完整工具描述与findUnapprovedCommand的 AST 守卫实现(含catechogrepjqlsmkdirperlsedtail的命令白名单)见 Appendix: Command Guard。注意:just-bash的解释器是 JS 实现,但文件系统是真实的——命令真的读写磁盘文件,这就是为什么命令守卫是这一路线的关键安全层。

组装 Agent 并运行

两条路线共用 Agent 接线方式:prepareCall钩子在每次 LLM 调用前重新读取 core 记忆并注入 system prompt:

import { ToolLoopAgent } from 'ai'; const today = new Date().toISOString().slice(0, 10); const memoryAgent = new ToolLoopAgent({ model: 'anthropic/claude-haiku-4.5', tools: { memory: memoryTool }, prepareCall: async settings => { // user-defined function fetches the contents of /.memory/core.md on every turn const coreMemory = await readCoreMemory(); return { ...settings, instructions: `Today's date is ${today}. Core memory: ${coreMemory} You can save and recall important information using the memory tool.`, }; }, });

因为prepareCall在工具循环的每次 generate 调用前都会执行,system prompt 始终反映core.md的最新状态:模型在对话中更新了 core 记忆,下一轮循环立即可见。

运行:

const prompt = 'Remember that my favorite editor is Neovim'; // Record the user message await appendConversation({ role: 'user', content: prompt, timestamp: new Date().toISOString(), }); // Run the agent (loops automatically on tool calls) const result = await memoryAgent.generate({ prompt }); // Record the assistant response await appendConversation({ role: 'assistant', content: result.text, timestamp: new Date().toISOString(), }); console.log(result.text);

如何验证:配方给出了典型交互序列——用户说"记住我最喜欢的编辑器是 Neovim",模型调用memory工具(如echo "- Favorite editor: Neovim" >> /.memory/core.md),工具执行并返回结果,模型确认后,下次运行时prepareCall读到该事实并注入 system prompt。据此可以核对三点:

  1. grep "Neovim" .memory/core.md(或notes.md)能查到刚写入的事实;
  2. 新开一轮对话提问(如"我偏好什么编辑器"),回答能命中该事实——这验证 core 记忆注入链路;
  3. conversations.jsonl中按轮次累积了 user/assistant 记录,grep -niE "pricing|budget" .memory/conversations.jsonl这类检索可验证 recall 记忆。

怎么选:按官方文档给出的条件对照

把三份文档中的条件收敛成判断路径:

  1. 已在用 Anthropic 模型,且想以最少代码获得记忆→ Provider-Defined Tools。实现只有一份execute函数,行为表现有厂商训练背书,但要接受仅 Claude 可用。
  2. 不想自建任何存储,接受外部服务依赖→ Memory Providers。再按约束细分:想让记忆随 Agent 运行时一起托管选 Letta;想要"套在任何 LLM provider 上"的记忆层、并能用addMemories/retrieveMemories显式管理选 Mem0;想要自动的语义搜索存取选 Supermemory;想自托管(Docker)并要五个显式记忆工具(含reflect综合、getDocument取文档)选 Hindsight。共同代价:provider 控制记忆行为,可见度低。
  3. 要完全掌握存储格式、接口和检索逻辑,且能承担前期实现成本→ Custom Tool。安全面(注入防护、路径白名单)由你自己负责;Route A 动作少而可控,Route B 灵活但必须上命令守卫。

两条硬边界也要记住:Provider-Defined Tools 与 Claude 绑定,属于文档明确声明的锁定点;MongoDB memory(@mongodb-developer/vercel-ai-memory,提供 Session/Semantic/Procedural/Episodic/Scratchpad 五级记忆)在官方文档中标注面向 AI SDK v6,依赖ai: ^6.0.0等 v6 API,如果你的项目不是 v6 就不能直接用,选择前先核对版本。

局限与后续

三条路径共同的验证逻辑是一样的:写入一条可识别的事实,跨对话取回,并检查存储侧(文件、provider 控制台或显式检索函数)确实留下了记录。Provider-Defined Tools 的execute与 Custom Tool 的存储层都是需要你自己实现的空缺,本文保留的是文档给出的完整接口与可直接执行的部分;Custom Tool 路线的完整参考实现(文件系统引导、结构化动作 handler、AST 命令守卫)集中在 07-custom-memory-tool.mdx 的附录中,可整体照搬。各 memory provider 的完整配置项(如 Letta 的maxStepstimeoutInSeconds,Hindsight 的budget参数表)见各自文档链接,接入时再展开即可。

【免费下载链接】aiThe AI Toolkit for TypeScript. From the creators of Next.js, the AI SDK is a free open-source library for building AI-powered applications and agents项目地址: https://gitcode.com/GitHub_Trending/ai/ai

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询