
最近在业务里做 AI Agent 落地时我一直在找一个能把“工具调用、记忆管理、类型安全”捏在一起的方案。大多数演示项目都是 Python 写一个脚本、调一次大模型接口就完事一旦要接入多个业务系统或者让 Agent 在长对话里记住关键上下文代码很快就会乱成一团。今天我选了一个挺有代表性的项目来完整拆解OneRingAI v1一个基于TypeScript的 Agent 运行时核心卖点是Integrations集成和Graph Memory图记忆。这篇教程会从一个开发者视角出发先讲清楚它解决什么问题再带你把环境、配置、核心代码完整跑起来最后整理我在实践过程中遇到的坑和工程建议。无论你是在调研 TS 技术栈的 Agent 框架还是想把已有业务工具接入 Agent都建议先收藏这篇文章。1. 背景与核心概念OneRingAI 到底是什么1.1 一句话理解 OneRingAIOneRingAI 是一个用 TypeScript 编写的 Agent 开发框架。它的目标不是再做一个“调用 ChatGPT API 的封装库”而是提供一套相对完整的运行时让你可以用配置和少量代码组装出一个具备工具调用能力和长期记忆能力的 AI Agent。名字里的 OneRing 很容易让人联想到“一枚戒指统领众戒”。放到技术语境里它的设计意图也类似把散落在各处的模型调用、业务 API、数据库、任务调度统一收口到一个订单式的运行时里开发者只需要面向一组类型定义和插件接口写代码。1.2 它解决的核心问题我们平时写一个 LLM 应用通常要面对这几件麻烦事模型调用不统一OpenAI、Anthropic、本地模型各有各的 SDK切换一次要改很多代码。工具集成靠硬编码让 Agent 调用搜索引擎、数据库、内部接口时往往需要自己拼接 JSON Schema维护成本很高。记忆模式太简单很多项目用“把历史消息全塞进 Prompt”来模拟记忆Token 消耗大而且东西一多就开始丢关键信息。缺乏可观测性Agent 为什么做出这个决策它沿途调用了哪些工具没有日志和中间状态排查问题非常痛苦。OneRingAI 的思路是把这些能力沉淀成框架级的抽象。你定义工具、定义记忆策略、定义模型来源框架负责调度和状态管理。加上 TypeScript 本身具备的类型系统工具入参、返回结果、配置结构都可以在编译期被检查运行时出错的概率会低很多。1.3 Graph Memory 是什么为什么值得关注在常见方案中记忆通常有两种形态KV 记忆直接存一个上下文对象适合短期、简单的会话状态。向量记忆把文本切片后做 Embedding查询时用语义相似度召回适合“模糊回忆”。图记忆Graph Memory把信息组织成“节点”和“边”。节点可以是实体、事件、任务边表示它们之间的先后、因果、包含等关系。Graph Memory 的优势在于可以记录关系。比如你和 Agent 聊到“A 项目依赖 B 服务”如果只存向量下次检索可能只找回一句孤立的话如果存成图就能明确知道Project A与Service B之间存在depends_on关系后续做任务拆解、影响分析时会更有依据。OneRingAI v1 把 Graph Memory 作为一等公民意味着你在构建 Agent 时可以直接决定“哪些信息该沉淀为节点哪些关联该记录为边”而不是把所有上下文都塞进 Prompt。2. 环境准备与版本说明在开始之前我先把本文用到的环境列出来。OneRingAI v1 是较新的项目API 可能随小版本调整所以下面以当前 v1 常见初始版本为准。你的项目如果版本不一致请以官方 README 和类型声明为准。2.1 本地环境清单项建议版本说明操作系统Windows / macOS / Linux 均可没有特殊平台依赖Node.js18 或 20 以上需要支持较新的 ES 特性pnpm / npm / yarnpnpm 8 或 npm 9包管理器任选TypeScript5.4 以上v1 依赖较新的类型能力大模型 API Key任意 OpenAI 兼容接口默认可用环境变量注入注意如果你本地 Node.js 版本偏低先升级到 18 以上再继续。我最初用 Node 16 跑时报了一堆NodeJS.ProcessEnv类型错误升级后问题消失。2.2 初始化 TypeScript 项目我建议先建一个干净目录然后初始化mkdir oneringai-demo cd oneringai-demo npm init -y npm install typescript types/node --save-dev npx tsc --init执行完npx tsc --init后会生成tsconfig.json。针对 Node tsx 开发场景建议把核心配置改成下面这样{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, outDir: ./dist, rootDir: ./src }, include: [src/**/*], exclude: [node_modules, dist] }这里特别提醒如果你在 TypeScript 5.x 下新建项目就不要再把baseUrl和paths写进tsconfig.json了。TypeScript 已经通过 deprecation 流程提示Option baseUrl is deprecated and will stop functioning in TypeScript 7.0.如果你早期项目里有类似配置请尽早改成相对路径或者用 NodeNext 的机制来解析模块。这一改动会影响很多历史项目建议搜一下自己的代码库看有没有把baseUrl: ./src写死在里面的配置文件。2.3 安装 OneRingAI 相关依赖安装完 TypeScript 后接着安装 OneRingAI。为了便于区分我用oneringai作为主包名下面的示例以 v1 初始版本为前提npm install oneringai npm install dotenv --save假设你的大模型接口支持 OpenAI 兼容协议那么环境变量可以这样准备# .env OPENAI_API_KEYsk-your-key OPENAI_BASE_URLhttps://api.openai.com/v1 DEFAULT_MODELgpt-4o-mini.env文件里不要写死敏感信息。项目里可以把.env.example提交到仓库把真实的.env加进.gitignore。这属于很小但很重要的工程习惯。3. 核心机制拆解Agent、Integrations、Graph MemoryOneRingAI v1 的核心抽象可以概括为三个关键词Agent 运行时、Integration 插件、Graph Memory 存储。下面逐个拆解。3.1 Agent 运行时官方把 Agent 定义为“一个可以感知环境、做出决策并执行动作的自治系统”。在 OneRingAI 中Agent 的职责边界是解析用户输入文本、结构化任务决定是否需要调用工具调用模型并处理模型产出的结构化动作将执行结果写回上下文和历史。用 TypeScript 角度来看一个 Agent 就是一个普通类或者函数式对象。它的核心类型大概是// 简化理解具体类型名以实际包为准 export interface AgentConfig { name: string; description: string; model: string; systemPrompt?: string; integrations?: Integration[]; memory?: MemoryConfig; } export interface AgentContext { input: string; threadId: string; metadata?: Recordstring, unknown; }我见过的初学者误区是总想把“业务逻辑”直接写进 Agent 的 System Prompt 里。其实 Agent 更适合做“调度者”具体的查询、计算、文件写入应该交给 Integration 去执行Agent 只负责判断“现在该调用哪个集成”。3.2 Integrations把业务能力插进 Agent“集成”是 OneRingAI 的另一个核心概念。你可以把它理解成可被 Agent 调用的工具集。每个 Integration 至少要描述三件事这个工具叫什么名字它接收什么参数执行后返回什么结果。用 JSON Schema 描述太啰嗦OneRingAI 提供了 TypeScript 类型让同一个对象既能作为编译期类型参与检查又能转换成模型需要的 Function Calling 格式。下面来看一个简化版的 Integration 定义// 文件路径src/integrations/calculator.ts import type { Integration } from oneringai; export const calculatorIntegration: Integration { name: calculator, description: 执行四则运算适合精确计算场景, inputSchema: { type: object, properties: { expression: { type: string, description: 例如 (12 34) * 5, }, }, required: [expression], }, async execute(input: { expression: string }) { // 安全边界正式环境建议用沙箱或表达式解析库而不是直接 eval const result Function(use strict; return (${input.expression}))(); return { result: Number(result) }; }, };注意上面那个Function只是一个示意。生产环境如果允许用户自定义表达式千万不要用 eval 或者 Function 来执行这是安全底线。建议换成mathjs的解析引擎或者只允许白名单语法。3.3 Graph Memory用节点和边组织记忆Graph Memory 在 OneRingAI v1 里提供几个关键 API创建/更新节点保存一个实体、事件创建/更新边保存两个节点之间的关系查询按节点 ID、关系类型、属性过滤检索给定当前问题召回最相关的子图作为上下文注入 Prompt。我把它理解成一个“面向 Agent 的嵌入式图数据库”。它不是类似 Neo4j 那样的服务端更多是内存中维护的图结构必要时可以持久化到文件。// 简化理解 export interface MemoryNode { id: string; type: entity | event | task; label: string; properties: Recordstring, unknown; createdAt: number; } export interface MemoryEdge { source: string; target: string; relation: string; weight?: number; }为什么用图而不是普通列表因为图更适合回答“谁和谁有什么关系”这类问题。比如ProjectX这个节点ServiceA这个节点一条边ProjectX - [depends_on] - ServiceA。当用户后续问“ProjectX 挂了会影响什么”Agent 可以通过图遍历快速找出关联的 ServiceA、ServiceB而不是在长文本里大海捞针。4. 完整实战构建一个带记忆的 TypeScript Agent接下来就是一个完整可运行的实战案例。我们要做的东西不复杂一个对话 Agent它能做精确计算能查询用户信息并且记得用户提到过的项目与服务的依赖关系。为了让你更好理解 OneRingAI 的设计我会先按“自建简化版”的方式把核心代码写出来。这些代码不依赖任何不存在的黑魔法完全是在 OneRingAI v1 的类型框架下组织业务逻辑。4.1 项目结构先按下面的结构创建文件├── .env ├── .gitignore ├── package.json ├── tsconfig.json ├── src/ │ ├── index.ts # 入口文件启动交互式 Agent │ ├── memory/ │ │ └── graph-memory.ts # 图记忆管理器 │ ├── integrations/ │ │ ├── calculator.ts # 计算工具 │ │ └── user-lookup.ts # 用户查询工具 │ └── agent/ │ └── chat-agent.ts # 组装 Agent4.2 创建图记忆管理器我们先实现一个轻量的图记忆管理器。这个文件只依赖 Node.js 自带的结构能力方便你理解图存储的算法本质。// 文件路径src/memory/graph-memory.ts import { randomUUID } from node:crypto; import type { MemoryNode, MemoryEdge } from oneringai; export class GraphMemoryStore { private nodes new Mapstring, MemoryNode(); private edges: MemoryEdge[] []; upsertNode(input: OmitMemoryNode, createdAt): MemoryNode { const existed this.nodes.get(input.id); const node: MemoryNode { ...input, createdAt: existed?.createdAt ?? Date.now(), }; this.nodes.set(input.id, node); return node; } addEdge(source: string, target: string, relation: string, weight 1) { const edge: MemoryEdge { id: randomUUID(), source, target, relation, weight, }; this.edges.push(edge); return edge; } getNode(id: string): MemoryNode | undefined { return this.nodes.get(id); } findRelated(sourceId: string, relation?: string): MemoryNode[] { return this.edges .filter((e) e.source sourceId (!relation || e.relation relation)) .map((e) this.nodes.get(e.target)) .filter((node): node is MemoryNode Boolean(node)); } /** 把与某个问题相关的节点文本拼出来用于注入 Prompt */ buildContext(seedId?: string): string { if (!seedId) return ; const seed this.nodes.get(seedId); if (!seed) return ; const related this.findRelated(seed.id); const relatedText related .map((item) - ${item.label}${item.type}) .join(\n); return [ 已知实体${seed.label}, relatedText ? 相关节点\n${relatedText} : , ---, ] .filter(Boolean) .join(\n); } listAll(): ArrayMemoryNode | MemoryEdge { return [...this.nodes.values(), ...this.edges]; } }要点说明upsertNode使用id作为主键重复写入会保留首次创建时间适合多次更新同一实体。findRelated用来做一层邻居查询。真实框架可能会做更深的遍历和相似度排序但核心思路一致。buildContext把图里的关键节点压缩成文本给 Prompt 提供上下文避免直接把完整图结构全塞进去。4.3 创建 Integration 工具紧接着我们实现计算工具和用户查询工具。// 文件路径src/integrations/calculator.ts import type { Integration } from oneringai; import { evaluate } from mathjs; export const calculatorIntegration: Integration { name: calculator, description: 执行四则运算表达式支持加减乘除和括号比如 (12 34) * 5, inputSchema: { type: object, properties: { expression: { type: string, description: 要计算的数学表达式, }, }, required: [expression], }, async execute(input: { expression: string }) { const result evaluate(input.expression); return { result }; }, };这里用mathjs的evaluate替换了之前的Function方案安全性好很多。安装一下npm install mathjs再看用户查询工具。为了演示方便我用一个内存数组模拟数据库。// 文件路径src/integrations/user-lookup.ts import type { Integration } from oneringai; const mockUsers [ { id: u_1, name: 张三, role: 前端工程师, project: 前端重构 }, { id: u_2, name: 李四, role: 后端工程师, project: 订单服务 }, ]; export const userLookupIntegration: Integration { name: user_lookup, description: 根据用户姓名查询用户基本信息包括角色和所属项目, inputSchema: { type: object, properties: { name: { type: string, description: 用户姓名 }, }, required: [name], }, async execute(input: { name: string }) { const user mockUsers.find( (item) item.name input.name || input.name.includes(item.name) ); if (!user) { return { found: false, message: 未找到用户${input.name} }; } return { found: true, user }; }, };在实际项目中你可以把user-lookup的实现替换成数据库查询或 HTTP 调用Integration 的接口不变上层 Agent 也不需要改动这就是“集成隔离”的价值。4.4 组装 Chat Agent接下来是核心部分把模型、工具、图记忆组装成一个 Agent。// 文件路径src/agent/chat-agent.ts import type { AgentConfig } from oneringai; import { calculatorIntegration } from ../integrations/calculator; import { userLookupIntegration } from ../integrations/user-lookup; import { GraphMemoryStore } from ../memory/graph-memory; export function createChatAgent(config: { model: string; systemPrompt?: string; }) { const memory new GraphMemoryStore(); // 这里演示如何把工具注册进 agent const agentConfig: AgentConfig { name: chat-agent, description: 一个支持计算、查询用户的对话 Agent, model: config.model, systemPrompt: config.systemPrompt ?? 你是团队助手。你可以使用工具完成计算和用户查询。 当用户提到“项目 A 依赖服务 B”这类关系时先在记忆中记录关系再回答用户。, integrations: [calculatorIntegration, userLookupIntegration], memory: { enabled: true, store: memory, }, }; return { agentConfig, memory, }; }真实的 OneRingAI 运行时会去调用模型 API并循环处理工具的返回结果。当我们不想在教程里依赖某个具体 SDK 时可以用一个模拟调用来演示完整流程。4.5 主入口模拟一次完整对话下面这段代码会把整个流程串起来接收用户输入、判断是否需要调用工具、记录图记忆、返回回答。// 文件路径src/index.ts import dotenv/config; import { createChatAgent } from ./agent/chat-agent; async function main() { const { agentConfig, memory } createChatAgent({ model: process.env.DEFAULT_MODEL ?? gpt-4o-mini, }); console.log(Agent: ${agentConfig.name}); console.log(准备接收输入模型${agentConfig.model}); console.log(-----------------------------------); // 模拟第一轮用户提到项目依赖关系 await handleUserInput( 帮我查一下李四的信息另外记一笔订单服务依赖支付服务 ); // 模拟第二轮用户询问依赖关系 await handleUserInput(订单服务依赖哪些服务); async function handleUserInput(input: string) { console.log(\n用户: ${input}); // 简化实现这里我们手动提取实体关系并写入图记忆 // 真实框架会通过 LLM 做实体抽取和关系判断 if (input.includes(依赖)) { const match input.match(/([\u4e00-\u9fa5A-Za-z0-9]服务)依赖([\u4e00-\u9fa5A-Za-z0-9]服务)/); if (match) { const source match[1]; const target match[2]; memory.upsertNode({ id: service:${source}, type: entity, label: source, properties: {}, }); memory.upsertNode({ id: service:${target}, type: entity, label: target, properties: {}, }); memory.addEdge(service:${source}, service:${target}, depends_on); const context memory.buildContext(service:${source}); console.log(\n[记忆] 已记录依赖关系当前上下文\n${context}); } } // 查询用户信息时调用工具 if (input.includes(查一下)) { const { user } await userLookupIntegration.execute({ name: 李四, }); console.log([工具 user_lookup] 返回, JSON.stringify(user)); } // 模拟模型回答 console.log(\n助手: 我已经完成处理。); } } main().catch((err) { console.error(运行失败, err); process.exit(1); });这段代码故意把“实体抽取”和“关系写入”写得很朴素目的是让你看清图记忆写入的位置和方式。真实项目中这一步通常由大模型返回的 Function Calling 结果驱动模型从对话里识别出实体和关系然后调用memory.addEdge。4.6 运行与预期输出在package.json里加一个启动脚本{ scripts: { dev: tsx src/index.ts, build: tsc, start: node dist/index.js } }安装tsxnpm install tsx --save-dev运行npm run dev正常输出大致如下Agent: chat-agent 准备接收输入模型gpt-4o-mini ----------------------------------- 用户: 帮我查一下李四的信息另外记一笔订单服务依赖支付服务 [记忆] 已记录依赖关系当前上下文 已知实体订单服务 相关节点 - 支付服务entity --- [工具 user_lookup] 返回{id:u_2,name:李四,role:后端工程师,project:订单服务} 助手: 我已经完成处理。 用户: 订单服务依赖哪些服务 [记忆] 已记录依赖关系当前上下文 已知实体订单服务 相关节点 - 支付服务entity ---从输出里可以看到第二次问答时Agent 直接拿着图记忆里的“相关节点”作为上下文而不是把第一轮所有原始文本再念一遍。这就是图记忆对上下文压缩的现实收益。5. 常见问题与排查思路在把 OneRingAI 接入项目的过程中我踩过不少坑。下面按问题现象整理成表格方便你排查。问题现象常见原因解决思路启动时报NodeJS.ProcessEnv类型不存在Node 类型定义没装或版本过旧安装types/node并确认tsconfig.json的types字段报错Option baseUrl is deprecated旧项目使用baseUrlpaths改用相对路径或NodeNext模块解析方式为 TypeScript 7.0 提前做准备模型一直不调用工具System Prompt 没有明确告知模型在 System Prompt 中说明“你可以使用工具”并给出工具的使用时机图记忆写入后查询不到节点 ID 不一致检查实体抽取的 ID 生成规则比如统一用service:${name}对话内容太长Token 超限未使用记忆压缩不要全量塞历史消息优先用图记忆或向量召回关键实体上下文工具报错导致整个 Agent 崩溃未对 Integration 做异常隔离在execute中捕获异常返回{ error: message }而不是抛出本地跑起来很慢每次启动都在重新构建开发时使用tsx避免每次改动跑完整tsc这里单独展开说一下baseUrl弃用的问题。TypeScript 5.x 里配置baseUrl时编译器可能直接提示Option baseUrl is deprecated and will stop functioning in TypeScript 7.0. Please use relative paths instead.很多从早期脚手架继承下来的项目都有这行配置。如果只是临时忽略警告项目还能跑但到了 TypeScript 7.0这个字段会失效。我的建议是尽早清理把import中的/xxx改成相对于当前文件的../xxx或者用包名映射的方式新建一个独立 npm workspace 包来承载业务模块不要新增依赖一个paths插件除非你做好长期维护的准备。6. 最佳实践与工程建议6.1 用类型系统管理工具协议OneRingAI 之所以适合 TypeScript 生态是因为你可以用类型来约束“模型能调用的工具”。建议在代码里定义一个共享的工具参数类型避免字符串类型满天飞export type ToolInput | { tool: calculator; expression: string } | { tool: user_lookup; name: string };这样当新增工具时TypeScript 会强制你在所有联合类型分支中补齐处理逻辑编译期就能发现遗漏。6.2 为记忆设计清晰的节点 ID 规范图记忆的有效性很大程度上依赖节点 ID 的一致性。如果同一实体在不同对话里生成不同 ID图就变成了一堆孤岛。建议实体类节点使用稳定前缀例如service:xxx、user:uid、project:xxx事件类节点使用时间戳加序号例如event:20250512-001写入边之前先查重避免同一条边重复累加。6.3 给 Integration 加授权与审计让 Agent 调用工具时默认遵循最小权限原则。比如查询工具只读不允许执行删除类操作外部 HTTP 集成只允许访问白名单域名所有工具调用记录日志包含入参摘要、返回结果状态、耗时涉及生产数据库的操作先走沙箱环境验证 SQL再开放权限。6.4 完善可观测性Agent 应用的调试难度远高于普通接口。我强烈建议从第一天就打印三类日志模型调用日志记录了发送给模型的 Prompt 中注入的记忆上下文工具调用日志记录了哪个工具被调用、入参是什么、耗时多少记忆变更日志记录了新增/更新了哪些节点和边。有了这三类日志当 Agent 回答“答非所问”时你能快速定位“上下文缺了”“工具选错了”还是“记忆写错了”。6.5 控制图记忆的增长图记忆虽然比文本记忆高效但也会无限增长。实践中建议设置策略节点数量超过阈值时清理properties里过期的字段边关系长期未被命中时降低权重甚至淘汰高频更新同一节点时保留最近 N 个版本即可。如果你只是做轻量演示OneRingAI 自带的内存管理够用若进入生产建议把图持久化到专门的图数据库或者定期序列化到文件并建立索引。7. 学习与后续拓展方向写到这里你应该已经把 OneRingAI v1 的核心概念、环境搭建、代码实践和排错思路完整过了一遍。从实际体验来看这个框架最值得学习的地方不是“又多了一个调模型的库”而是它对 Agent 结构化能力的整理工具集成用 Integration 抽象记忆用 Graph Memory 抽象剩下的交给运行时调度。如果接下来你想继续深入我建议按这个顺序推进认真读一遍官方 TypeScript 类型声明。这是最准确、也最容易被人忽略的文档。把示例里的工具替换成真实业务接口。比如把user-lookup改成查询真实数据库记录一下改造难度你会直观感受到“集成隔离”的价值。研究 Function Calling 的循环流程。认真读 Agent 运行时里面“模型返回工具调用 - 执行工具 - 返回结果给模型”的循环这决定了你对 Agent 行为的把控力。结合向量记忆做混合检索。图记忆适合结构关系向量记忆适合语义模糊查询两者结合是生产级 Agent 的常见形态。在工程落地上我更想提醒的是不要一上来就想做“全自主 Agent”。先把一个业务闭环跑通把工具边界、权限边界、记忆策略定清楚再逐步放开 Agent 的自主决策范围。这样即使模型偶尔判错影响范围也是可控的。如果你在实践 OneRingAI 或类似 TypeScript Agent 框架时遇到任何问题欢迎在评论区把报错信息发出来我们一起看看是类型层面的问题还是图记忆设计的问题。也建议先把这篇文章收藏起来等真正开始搭 Agent 项目时按步骤对照来能少走不少弯路。