TypeScript构建智能体:图记忆与工具集成实践
发布时间:2026/8/30 21:36:26来源:尧图网络
你如果最近在关注 LLM Agent 生态大概会注意到一个趋势Python 仍然是训练和实验的主力但真正要把 Agent 部署到业务系统里时越来越多团队开始选择 TypeScript。这种情况下看到 OneRingAI v1 这个项目出现在 Hacker News 上很容易让人眼睛一亮。它的定位非常干脆TypeScript agents with integrations and graph memory。先给一个判断这类项目真正值得关注的点不是“又出了一个新的 Agent 框架”而是它把一个经常被忽略的问题摆到了台面上——Agent 产品要落地必须同时解决外部工具接入和长期记忆两个问题。只接工具但没有记忆Agent 就像失忆的实习生有记忆但接不上业务系统它又只是一堆闲聊代码。OneRingAI 把这两件事放在一起做方向是对的但工程上能不能站稳还要看它的记忆抽象、工具协议和类型设计。这篇文章不会假装自己有一份官方文档然后替你翻译。公开材料有限的情况下更重要的是把 OneRingAI 背后代表的设计逻辑讲清楚为什么 TypeScript 适合写 Agentgraph memory 到底解决了什么integrations 在真实工程里应该怎么设计。然后我会带你实现一个可运行的最小 TypeScript Agent包含图记忆和工具调用你可以直接跑起来验证。想要踩坑的人也能在后面的常见问题部分找到排查方向。1. 为什么 TypeScript 正在成为 Agent 开发的新选择1.1 Python 阵营的优势与隐性成本过去两年大部分 Agent 项目的起点都是 Python。原因很明显模型 SDK、向量库、LangChain、LlamaIndex 这些生态最早都在 Python 社区成熟论文复现和实验脚本也都是 Python 写的。如果你只想快速验证“大模型能不能完成某个任务”用 Python 写几十行脚本确实是最短路径。但问题在于短路径不代表好的工程路径。当一个 Agent 从 Notebook 走进生产环境它需要面对的不再是“模型会不会答”而是权限控制、接口协议、日志链路、配置管理、部署方式、团队协作这些琐碎问题。在这些方面Python 的动态类型带来很大负担。一个 function call 的参数到底有哪些字段IDE 无法帮你检查只能靠运行时日志。Agents 的调用链路特别长模型返回、工具执行、记忆命中、异常重试每一步都是隐式数据结构类型一塌糊涂排查问题就像在断电线里找哪根有问题。1.2 TypeScript 的真正优势不是类型而是同构TypeScript 在 LLM 编程里的优势经常被简单概括成“有类型”。但更准确地说它的优势是“端到端类型 全栈同构”。业务系统如果是 Node.js 或前端体系用 TypeScript 写 Agent意味着从浏览器端的事件捕获、到后端 API、到 Agent 的 tool 定义、再到数据库访问层可以共用同一套类型定义。比如一个“查询订单”的工具前端表单字段和后端查询参数可以同一套 interface 约束。对这种场景客户端、服务端、Agent 工具三者不必在 JSON 上来回手写转换TypeScript 的 inference 和 compile-time check 能把集成错误提前拦住一大半。另外越来越多的应用运行环境本身就基于 JS 运行时。Electron 桌面端、Edge 插件、Chrome Extension、小程序前端、Node 服务端都可以直接在已有代码里嵌入一个 TypeScript Agent。Python 在这个链路里通常只能作为独立服务部署集成成本高出一个数量级。1.3 什么时候不要用 TypeScript 写 Agent需要说清楚的是TypeScript 不是万能选择。如果你的核心任务是研究模型推理、做 prompt 实验、训练小模型或者依赖 only 存在于 Python 生态的库那就不要为了统一语言而硬切。TypeScript Agent 更适合的是“把模型能力包进已有软件系统”的场景而不是“从零研究模型能力”的场景。一个更稳妥的组合方式是Python 侧负责离线实验和模型服务TypeScript 侧负责 Agent 编排、工具调用和记忆管理。很多团队就是这么做的OpenAI SDK 和各类开放模型服务都支持在 Node.js 里直接调用并没有想象中那么困难。2. OneRingAI v1 的定位三个关键词拆解2.1 TypeScript agents把 “TypeScript agents” 单独拿出来看它要表达的意思其实是Agent 的构建过程应该是有类型约束的而不是把所有数据结构都塞进一个宽松的any。一个 Agent 的典型状态包括对话历史、工具调用记录、记忆上下文、任务状态、结果缓存。如果每个状态都是一个没有 schema 的 JSON 对象项目大了以后几乎没有可维护性。TypeScript Agent 的意义就在这里它强迫你在写主循环之前先定义消息类型、工具返回值类型、记忆节点类型。这些定义本身就是文档也是协作边界。2.2 integrationsAgent 如果只会对话那么它的价值非常有限。真实业务里用户问“帮我查一下订单物流”Agent 需要调订单服务用户问“给客户发一封邮件”Agent 需要调邮箱用户问“分析这个仓库的安全风险”Agent 需要调代码扫描 API。这些都是 integrations。OneRingAI 把 integrations 作为一级关键词说明它希望覆盖的不仅是文本对话而是能操作真实系统的 Agent。这种设计方向是对的但集成能力要做到“开箱即用”并不容易。不同服务有不同的鉴权方式、限流策略、错误码和数据结构统一抽象越深通用性越好但具体服务的个性能力越容易丢失。这里面的平衡点是判断一个 Agent 框架是否成熟的关键。2.3 graph memory记忆是 OneRingAI 的第二个关键词也是它最值得研究的部分。普通 Agent 记忆通常是一个文本列表或一个向量库但 graph memory 把记忆描述成节点和边用户是一个节点电影是一个节点“喜欢”是一条边。这种结构的最大好处是支持多跳关联查询。举个例子用户说“我喜欢《沙丘》”。如果只存入向量库再过几天用户问“推荐一部氛围类似的电影”向量检索大概率能找到“科幻、史诗”这类相似语义。但如果用户问“我上次提到的那位导演的另一部作品”向量库就很难处理了因为它不是一个语义相似问题而是一个关系路径问题用户 - 喜欢 - 电影 - 导演 - 另一部作品。只有图结构能高效回答这种问题。2.4 它和 LangGraph.js、MCP 的关系很多人看到 “agents graph memory” 会想到 LangGraph.js。区别在于LangGraph.js 是一个完整的图编排框架它的核心是控制流的图节点是状态转换边是状态跳转。OneRingAI 从项目名称看重点更像是“记忆图 外部工具”控制流不一定那么复杂。这其实也是对的方向因为大量 Agent 任务并不需要复杂状态机只需要一个可靠的主循环。同时我也希望它支持 MCP 这类开放式工具协议。如果工具定义能兼容 MCP那么它就能以极低成本接入社区里越来越多的 MCP Server。如果你正在评估这个项目建议优先看它的工具抽象有没有走这种开放协议而不是内置了几个固定的 API 集成。3. Agent 记忆从 Buffer 到 Vector再到 Graph3.1 没有记忆的 Agent 是什么状态很多入门项目把大模型 API 封装成一个问答接口用户每次提问都直接调模型。这种 Agent 的问题非常明显用户刚说了自己的偏好下一次提问又忘了。你问它“我老板姓什么”它会一本正经地告诉你“我不记得我们聊过这个问题”。这种体验在一次性问答场景里可以接受但任何涉及个性化、多轮任务、长期服务的场景都不能接受。最简单的记忆实现是滑动窗口把最近几轮对话拼进上下文字符串。它实现成本低LLM 能够通过上下文看到刚才的内容。但问题也明显窗口太短记不住远的事实窗口太长又会超过模型上下文限制并且成本成倍增长。3.2 三种记忆方案对比记忆方案实现成本关联查询能力适合场景无状态最低无一次性问答滑动窗口低只能记住最近几轮连续对话、客服助手向量检索中按语义相似度召回知识库问答、长文档检索图记忆高支持多跳关系查询用户画像、关系推理、跨会话记忆注意这里的“高”不代表不能做。对于中小型项目graph memory 可以先用内存中的邻接表实现等规模上来再换成 Neo4j 或 ApsaraDB 图引擎。关键是先让业务跑通而不是一开始就引入重组件。3.3 graph memory 为什么适合 Agent图记忆的价值可以用一个真实场景说明。假设你在做一个 CRM 助手用户是销售他在三天前提过“下个月要拜访华东区客户”。今天他问“把华东区客户里上个月和我吃过饭的人都列出来。”这个问题有两个约束条件地域在华东、关系是上个月吃过饭。如果用向量库你只能把两个条件拼成一个 query 去检索召回结果可能不完整。如果用图记忆你会预先存储销售和客户之间有一顿“聚餐”的边时间属性写着“2025-05”。查询时从销售节点出发先通过“聚餐”边找到关联客户再用地域属性过滤整个过程清晰、可控、可解释。更重要的是图记忆可以持续增长每次对话都往里面加节点和边Agent 的知识就越来越丰富。3.4 graph memory 的代价代价也不能回避。图记忆最大的问题不是存储而是“谁来建图”。如果你让 LLM 从对话里抽取实体和关系抽取质量会直接影响后续查询。抽错一个实体可能把完全不相干的两个信息连在一起。此外图数据也会膨胀如果不做剪枝和过期处理几十万节点之后BFS 查询也会变慢。所以工程上必须在写入阶段做过滤而不是把每句话都塞进记忆图。4. 环境准备与最小工程结构4.1 环境要求本项目的示例代码不需要 GPU只需要一个可访问的模型 API。建议环境如下Node.js 18 及以上版本当前使用 20 或 22 会更稳TypeScript 5.x 最新版本一个 OpenAI 兼容的模型服务直接使用 OpenAI API Key 也可以npm 或 pnpm 等任意包管理工具如果你所在团队已经有内部模型网关只要它兼容 Chat Completions 协议同样可以复用下面的代码只需要把baseURL指向你自己的服务。4.2 初始化项目先创建目录并初始化项目mkdir ts-agent-graph-memory-demo cd ts-agent-graph-memory-demo npm init -y npm install openai npm install -D typescript tsx types/nodeopenai是官方 Node SDK后面生成对话、调用工具都靠它。tsx用来直接运行 TypeScript 文件避免每次先编译再运行开发时很高效。4.3 tsconfig 注意事项执行npx tsc --init会生成一个默认配置文件但建议改成下面这样{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, strict: true, outDir: dist, rootDir: src, skipLibCheck: true }, include: [src] }注意到这个配置里没有baseurl。原因是近期 TypeScript 已经明确把baseurl标记为不建议继续使用未来可能在 TypeScript 7.0 中停止生效。旧项目的tsconfig.json里经常会见到baseurl: ./配合paths使用新项目不要再依赖这个写法尽量用 NodeNext 模块解析或直接使用相对路径。4.4 依赖安装后的提示安装完成后建议先看一次package.json内容确认type: module是否设置。如果你希望用 ESM 方式写代码可以把type加进去{ name: ts-agent-graph-memory-demo, private: true, version: 0.1.0, type: module, scripts: { dev: tsx src/index.ts, build: tsc, start: node dist/index.js } }如果type不加代码里又使用import语法tsx 也能运行但 Node 直接跑编译产物时可能报错。这里建议统一使用 ESM。5. 完整示例用 TypeScript 实现带图记忆的 Agent这一节我们实现一个最小但可运行的 Agent。它会包含两块图记忆的存储和查询以及 Agent 调用 LLM 生成回答的主循环。为了让代码可控我故意没有引入 LangChain 这类重型框架你跑通以后可以再对比 OneRingAI 的抽象方式。5.1 定义 MemoryGraph文件路径src/memoryGraph.tsexport interface MemoryNode { id: string; type: string; properties: Recordstring, unknown; } export interface MemoryEdge { source: string; target: string; relation: string; properties?: Recordstring, unknown; } export class MemoryGraph { private nodes new Mapstring, MemoryNode(); private edges: MemoryEdge[] []; get size(): number { return this.nodes.size; } addNode(node: MemoryNode): void { this.nodes.set(node.id, node); } addEdge(edge: MemoryEdge): void { if (!this.nodes.has(edge.source) || !this.nodes.has(edge.target)) { throw new Error(添加边失败节点 ${edge.source} 或 ${edge.target} 不存在); } this.edges.push({ ...edge }); } search(keyword: string): MemoryNode[] { const lower keyword.toLowerCase(); return [...this.nodes.values()].filter((node) JSON.stringify(node).toLowerCase().includes(lower) ); } neighbors(nodeId: string, maxDepth 2): Setstring { const visited new Setstring(); if (!this.nodes.has(nodeId)) return visited; const queue: Array{ nodeId: string; depth: number } [{ nodeId, depth: 0 }]; visited.add(nodeId); while (queue.length 0) { const current queue.shift()!; if (current.depth maxDepth) continue; for (const edge of this.edges) { if (edge.source current.nodeId !visited.has(edge.target)) { visited.add(edge.target); queue.push({ nodeId: edge.target, depth: current.depth 1 }); } if (edge.target current.nodeId !visited.has(edge.source)) { visited.add(edge.source); queue.push({ nodeId: edge.source, depth: current.depth 1 }); } } } return visited; } recall(question: string, maxDepth 2): string { const matchedNodes this.search(question); const collected new Setstring(); for (const node of matchedNodes) { collected.add(node.id); for (const neighborId of this.neighbors(node.id, maxDepth)) { collected.add(neighborId); } } return [...collected] .map((id) JSON.stringify(this.nodes.get(id))) .join(\n); } }这个实现很短但包含了图记忆的核心节点存储、边存储、关键词命中、邻接扩展。recall方法先用关键词找到种子节点再通过 BFS 扩展到最多两层邻居最终把相关节点拼成上下文文本。这就是“图记忆召回”的最小形态。5.2 用 LLM 把事实写入图记忆文件路径src/agent.ts光有图结构还不够还需要把对话中的事实写进图里。最简单的方式是让 LLM 做“知识抽取”把一句话变成节点和边。import OpenAI from openai; import { MemoryGraph } from ./memoryGraph; const client new OpenAI({ apiKey: process.env.OPENAI_API_KEY ?? no-key, }); export async function saveFactToGraph(fact: string, graph: MemoryGraph) { const extraction await client.chat.completions.create({ model: process.env.OPENAI_MODEL ?? gpt-4o-mini, response_format: { type: json_object }, messages: [ { role: system, content: 你是知识抽取器。把用户事实转换成 JSON格式 {nodes:[{id:实体唯一ID,type:实体类型,properties:{name:展示名}}],edges:[{source:主体ID,target:客体ID,relation:关系}]}。 只输出 JSON。 }, { role: user, content: fact } ] }); const content extraction.choices[0].message.content ?? {}; const parsed JSON.parse(content) as { nodes?: Array{ id: string; type: string; properties: Recordstring, unknown }; edges?: Array{ source: string; target: string; relation: string }; }; for (const node of parsed.nodes ?? []) { graph.addNode(node); } for (const edge of parsed.edges ?? []) { graph.addEdge(edge); } return { ok: true, nodes: parsed.nodes ?? [], edges: parsed.edges ?? [] }; }这里用到了response_format: { type: json_object }它要求模型按 JSON 输出。使用 JSON mode 时系统提示词里必须包含“JSON”这个词所以我在提示词里明确写出了“把用户事实转换成 JSON”。5.3 Agent 主循环召回 → 思考 → 工具调用 → 写入继续编辑src/agent.ts加入 Agent 主循环和 remember 工具。const SYSTEM_PROMPT 你是一个带图记忆的智能助手。请结合“记忆上下文”中给出的用户事实回答问题。 如果上下文为空就正常回答。回答要简洁、直接。; const tools: OpenAI.Chat.Completions.ChatCompletionTool[] [ { type: function, function: { name: remember, description: 把一条重要事实保存到图记忆例如用户偏好、人物关系、项目背景。, parameters: { type: object, properties: { fact: { type: string, description: 要保存的完整事实描述。 } }, required: [fact] } } } ]; export async function runAgentLoop( userInput: string, graph: MemoryGraph ): Promisestring { const recalled graph.recall(userInput, 2); const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] [ { role: system, content: ${SYSTEM_PROMPT}\n\n记忆上下文\n${recalled || 无} }, { role: user, content: userInput } ]; for (let step 0; step 5; step) { const response await client.chat.completions.create({ model: process.env.OPENAI_MODEL ?? gpt-4o-mini, messages, tools, }); const message response.choices[0].message; messages.push({ role: assistant, content: message.content, tool_calls: message.tool_calls as never }); if (!message.tool_calls || message.tool_calls.length 0) { return message.content ?? ; } for (const call of message.tool_calls) { if (call.function.name remember) { const args JSON.parse(call.function.arguments || {}) as { fact?: string }; const saved await saveFactToGraph(args.fact ?? , graph); messages.push({ role: tool, tool_call_id: call.id, content: JSON.stringify(saved) }); } } } throw new Error(Agent 达到最大步数限制); }这段代码逻辑很简单每次进入循环先根据用户输入从图记忆里召回相关实体再调用模型。如果模型返回tool_calls说明它想保存新事实代码就执行saveFactToGraph写入图记忆然后把结果作为 tool 消息推回给模型。如果模型没有要求调用工具就直接返回回答。5.4 入口文件与运行验证文件路径src/index.tsimport { MemoryGraph } from ./memoryGraph; import { runAgentLoop } from ./agent; async function main() { if (!process.env.OPENAI_API_KEY) { console.error(请先设置 OPENAI_API_KEY 环境变量); process.exit(1); } const graph new MemoryGraph(); graph.addNode({ id: user-1, type: person, properties: { name: 访客 } }); graph.addNode({ id: movie-dune, type: movie, properties: { title: 沙丘, genre: 科幻 } }); graph.addEdge({ source: user-1, target: movie-dune, relation: likes, properties: { time: 今天 } }); console.log(记忆图节点数量:, graph.size); const question process.argv[2] || 我上次说我喜欢什么电影请推荐一部氛围类似的。; const answer await runAgentLoop(question, graph); console.log(\nAgent 回复:\n answer); } main().catch((error) { console.error(error); process.exit(1); });运行命令export OPENAI_API_KEY你的密钥 export OPENAI_MODELgpt-4o-mini npx tsx src/index.ts
网站建设高端定制企业官网