分析 Rome 的 TypeScript 实现,TaoToken 管模型 Key

分析 Rome 的 TypeScript 实现,TaoToken 管模型 Key 1. Rome 的 TypeScript 模型层拆开「递归 agent」的 Key 注入点把 Rome 这类 TypeScript agent OS 接进自己的工具链第一步不是改 agent 循环而是把模型 Key 从内核里拿出来。你可以先从 TaoToken 官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentrome-ts-intro 申请一个 Key再让 Rome 的模型层只认环境变量。Rome 的定位是给人和 agent 共用的 agentic OS它想解决的是 agent 每次对话都从零开始、不沉淀经验、不累积能力的问题。从 TypeScript 代码分析的角度看这个愿景能不能落地很大程度上取决于模型层有没有被切成干净的可替换组件。如果把 Rome 当成一个 TypeScript monorepo 来读最值得先看的是三层边界第一层是 agent runtime负责循环、工具调用、状态推进第二层是 memory 与 state负责把每次运行的结果写回可复用的上下文第三层是 model provider负责和外部模型服务通信。很多人会把 Key 直接塞进 runtime 的配置里或者在每个工具函数里单独 new 一个 SDK 客户端。这种写法在 demo 阶段跑得通但一旦要做多模型调度、成本核算、权限隔离就会立刻变成技术债。Rome 的「递归 agent」要能自我改进前提是每次模型调用都可追踪、可替换、可计量。Key 的来源如果不统一后面的 token 账本、模型切换、团队协作都会变得不可控。TaoToken 在这个位置的价值不是替 agent 做决策而是把模型访问收敛到一个入口。Base URL 用 https://taotoken.net/apiKey 用 YOUR_API_KEY 占位代码里只读环境变量。这样 Rome 的模型层就变成无状态的适配器runtime 不关心模型来自哪家memory 不关心凭证怎么轮换工具层也不接触密钥。对于想研究 agent 架构的开发者来说这种切分比单纯跑通一次对话重要得多。层级应该负责不应该负责agent runtime循环、计划、工具调度模型 Key、供应商细节memory/state摘要、持久化、检索直接发模型请求model provider请求封装、重试、usage 解析业务逻辑、工具执行配置层Key、Base URL、默认模型写进 agent 内核上表的边界不是 Rome 独有而是任何想从「一次性工具」走向「可累积系统」的 agent 项目都绕不开的。Rome 的 TypeScript 实现如果要在后期支持多种模型、多个租户、多套凭证模型层就必须先做成接口。下面从一个最小可运行的 TypeScript provider 开始把 TaoToken 的 Key 管起来。2. 用 TypeScript 实现可替换的 ModelProvider只认 TAOTOKEN_API_KEYRome 的模型层如果要支持多供应商第一步是定义接口。接口不需要复杂但要能覆盖 base URL、api key、默认模型、超时和 usage 返回。下面的代码是一个可运行的最小示例依赖 OpenAI 兼容 SDK。TaoToken 的模型入口使用 https://taotoken.net/api不要在代码里写死其他地址。Key 从环境变量读取占位符统一用 YOUR_API_KEY。// src/model/provider.ts export interface ChatMessage { role: system | user | assistant | tool; content: string; } export interface ModelUsage { inputTokens: number; outputTokens: number; totalTokens: number; model: string; requestId?: string; } export interface ModelProvider { id: string; baseURL: string; defaultModel: string; chat(input: { messages: ChatMessage[]; model?: string; temperature?: number; signal?: AbortSignal; }): Promise{ content: string; usage: ModelUsage; raw: unknown; }; }// src/model/taotoken-provider.ts import OpenAI from openai; import type { ChatMessage, ModelProvider, ModelUsage } from ./provider; function requireEnv(name: string): string { const value process.env[name]; if (!value) { throw new Error(Missing environment variable: ${name}); } return value; } export class TaoTokenProvider implements ModelProvider { id taotoken; baseURL process.env.TAOTOKEN_BASE_URL ?? https://taotoken.net/api; defaultModel process.env.TAOTOKEN_MODEL ?? YOUR_MODEL_ID; private client: OpenAI; constructor() { this.client new OpenAI({ apiKey: requireEnv(TAOTOKEN_API_KEY), baseURL: this.baseURL, timeout: 60_000, maxRetries: 2, }); } async chat(input: { messages: ChatMessage[]; model?: string; temperature?: number; signal?: AbortSignal; }) { const model input.model ?? this.defaultModel; const completion await this.client.chat.completions.create( { model, messages: input.messages, temperature: input.temperature ?? 0.2, stream: false, }, { signal: input.signal }, ); const choice completion.choices[0]; const usage completion.usage; const result: ModelUsage { inputTokens: usage?.prompt_tokens ?? 0, outputTokens: usage?.completion_tokens ?? 0, totalTokens: usage?.total_tokens ?? 0, model, requestId: completion.id, }; return { content: choice?.message?.content ?? , usage: result, raw: completion, }; } }对应的.env.example可以这样写# TaoToken 模型入口固定使用该 Base URL TAOTOKEN_BASE_URLhttps://taotoken.net/api # 从 TaoToken 控制台创建的 Key本地只放占位符 TAOTOKEN_API_KEYYOUR_API_KEY # 默认模型 ID 以 TaoToken 控制台模型列表为准 TAOTOKEN_MODELYOUR_MODEL_ID运行方式很简单本地安装依赖后直接用 tsx 跑一个 smoke testnpm init -y npm i openai npm i -D typescript tsx types/node cat .env EOF TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEYYOUR_API_KEY TAOTOKEN_MODELYOUR_MODEL_ID EOF npx tsx src/model/smoke.ts// src/model/smoke.ts import dotenv/config; import { TaoTokenProvider } from ./taotoken-provider; async function main() { const provider new TaoTokenProvider(); const res await provider.chat({ messages: [ { role: system, content: 你是一个只输出 JSON 的助手。 }, { role: user, content: 输出一个字段 ok值为 true。 }, ], }); console.log(res.content); console.log(res.usage); } main().catch((err) { console.error(err); process.exit(1); });这段代码的关键点不是 SDK 本身而是把 Key 注入点收在构造函数里。Rome 的 runtime 如果要支持多个 provider只需要在启动时注册不同的 ModelProvider 实现。TaoToken 的 Key 从官网获取入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentrome-ts-key 。创建 Key 时不要把它提交到仓库本地用.envCI 里用 secret manager。模型名不要猜直接看控制台可用列表否则会在下一节排障里遇到 404。3. 给递归 agent 记账在模型层拦截 usage 并落盘 JSONLRome 想做「越用越强」的 agent那么 token 消耗就不能只是一次性日志。递归 agent 每一轮都会产生模型调用每一轮都可能写入 memory。如果只记录最终结果不记录中间成本就无法判断这个 agent 到底是在复利还是在空转。更实际的做法是在模型层外面包一层 usage hook把每次调用的输入 token、输出 token、模型、时间戳、调用来源写成 JSONL。JSONL 的优点是追加写、易解析、不需要数据库适合本地开发阶段。下面这个包装器不改变 ModelProvider 接口只拦截chat的返回。// src/model/with-usage-log.ts import { appendFileSync, mkdirSync } from node:fs; import { dirname } from node:path; import type { ModelProvider } from ./provider; export interface UsageRecord { ts: string; provider: string; model: string; inputTokens: number; outputTokens: number; totalTokens: number; requestId?: string; traceId: string; scene: string; } export function withUsageLog( provider: ModelProvider, options: { filePath: string; traceId: string; scene: string; }, ): ModelProvider { return { ...provider, async chat(input) { const started Date.now(); const result await provider.chat(input); const record: UsageRecord { ts: new Date().toISOString(), provider: provider.id, model: result.usage.model, inputTokens: result.usage.inputTokens, outputTokens: result.usage.outputTokens, totalTokens: result.usage.totalTokens, requestId: result.usage.requestId, traceId: options.traceId, scene: options.scene, }; mkdirSync(dirname(options.filePath), { recursive: true }); appendFileSync(options.filePath, ${JSON.stringify(record)}\n, utf8); console.log([usage] ${Date.now() - started}ms ${record.totalTokens} tokens); return result; }, }; }调用侧可以这样写// src/agent/minimal-run.ts import dotenv/config; import { TaoTokenProvider } from ../model/taotoken-provider; import { withUsageLog } from ../model/with-usage-log; async function main() { const baseProvider new TaoTokenProvider(); const provider withUsageLog(baseProvider, { filePath: .taotoken/usage.jsonl, traceId: run_${Date.now()}, scene: rome_style_agent_loop, }); const memory: string[] []; for (let step 0; step 3; step) { const res await provider.chat({ messages: [ { role: system, content: 你是 Rome 风格的递归 agent每次只输出下一步动作。 }, { role: user, content: 已有记忆${memory.join( | ) || 空}。请给出第 ${step 1} 步动作。 }, ], }); memory.push(res.content); console.log(step ${step 1}:, res.content); } } main().catch((err) { console.error(err); process.exit(1); });跑完之后.taotoken/usage.jsonl会类似这样{ts:2026-01-01T10:00:00.000Z,provider:taotoken,model:YOUR_MODEL_ID,inputTokens:42,outputTokens:18,totalTokens:60,requestId:req_xxx,traceId:run_1760000000000,scene:rome_style_agent_loop} {ts:2026-01-01T10:00:03.000Z,provider:taotoken,model:YOUR_MODEL_ID,inputTokens:81,outputTokens:22,totalTokens:103,requestId:req_yyy,traceId:run_1760000000000,scene:rome_style_agent_loop}有了这份账本你就能做三件事第一看递归轮次增加时 token 是否线性膨胀第二比较不同模型在同一个 agent 循环里的成本第三把高消耗的 trace 和 memory 写入关联起来判断哪些记忆真正有用。Rome 的「复利」不是口号它需要这种可观测性。Key 仍然只从环境变量读取Base URL 仍然是 https://taotoken.net/api 不在业务代码里出现。4. Claude Code、Codex、CC Switch 三件套本地工具切到 TaoToken很多开发者不是只在自研 TypeScript 项目里用模型还会同时用 Claude Code、Codex CLI、CC Switch 这类本地工具。Rome 的模型层思路可以迁移过去把供应商地址、Key、默认模型三件事分开配置。注意Claude Code 使用 ANTHROPIC_变量Codex 使用 config.toml不要把 ANTHROPIC_套到 Codex 上。**Claude Code 的settings.json可以这样配{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: YOUR_API_KEY, ANTHROPIC_MODEL: YOUR_MODEL_ID } }如果你使用项目级配置可以放在.claude/settings.local.json{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: YOUR_API_KEY, ANTHROPIC_MODEL: YOUR_MODEL_ID } }Codex CLI 用config.toml不要混用 Anthropic 变量model YOUR_MODEL_ID model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chat然后在本机 shell 里设置 Keyexport TAOTOKEN_API_KEYYOUR_API_KEYWindows PowerShell 可以用$env:TAOTOKEN_API_KEYYOUR_API_KEYCC Switch 三件套可以理解为供应商条目、模型条目、认证注入。对齐方式如下供应商 Base URL 填https://taotoken.net/api默认模型填 TaoToken 控制台里的模型 ID认证环境变量填YOUR_API_KEY并确认 CC Switch 写到了正确的settings.json或项目级配置文件。如果你在 CC Switch 里维护多套配置建议命名成「taotoken-dev」「taotoken-review」「taotoken-agent」这类场景名而不是「default」。因为 Rome 风格的 agent 循环和普通对话的 token 消耗差异很大场景分离后更容易排查。Claude Code 的更多环境变量和行为说明可以看官方文档入口https://taotoken.net/doc/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_contentrome-ts-cc-doc 。配置完成后先用一个最小问题验证不要直接跑长任务。5. 排障401、模型名不匹配、流式断连的定位路径接入模型层时最常见的不是大问题而是几个配置点没对齐。下面按现象给定位路径。排障前可以先从 TaoToken 官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentrome-ts-troubleshoot 确认 Key 状态和可用模型。现象一401 或 403。先在本地确认环境变量是否真的被进程读到node -e console.log(process.env.TAOTOKEN_API_KEY ? key exists : key missing) node -e console.log(process.env.TAOTOKEN_BASE_URL || base url missing)如果 Key 存在再看请求头是否是Authorization: Bearer YOUR_API_KEY。有些 SDK 会读取OPENAI_API_KEY但我们的 provider 明确要求TAOTOKEN_API_KEY所以不要依赖默认变量。Claude Code 用的是ANTHROPIC_AUTH_TOKENCodex 用的是TAOTOKEN_API_KEY两条链路不要互相抄。现象二404 或 model not found。大概率是模型名写错。TypeScript provider 里默认模型来自TAOTOKEN_MODEL如果这个值还是YOUR_MODEL_ID请求就会失败。解决方式是登录控制台复制可用模型 ID替换.env。不要在代码里写一个猜测的模型名也不要把 Claude Code 的模型名直接填到 Codex 配置里。现象三429 或频繁超时。先降低并发。Rome 风格的递归 agent 如果每轮都并发调用工具和模型很容易在本地把并发打满。可以在 provider 外面加一个简单的信号量// src/model/semaphore.ts export class Semaphore { private queue: Array() void []; private active 0; constructor(private limit: number) {} async runT(fn: () PromiseT): PromiseT { if (this.active this.limit) { await new Promisevoid((resolve) this.queue.push(resolve)); } this.active; try { return await fn(); } finally { this.active--; const next this.queue.shift(); if (next) next(); } } }const sem new Semaphore(2); const res await sem.run(() provider.chat({ messages }));现象四流式响应断连。如果你的 provider 打开stream: true但没有正确消费 SSE连接会看起来像卡死。先关掉流式用非流式跑通再逐步打开流式。另外检查超时时间默认 60 秒对长输出可能不够可以按场景调整到 120 秒但不要无限等待。所有命令都在本地执行不要把 agent 直接连到生产数据库或生产系统。6. 可复现验证从 Key 到一次 Rome 风格的最小 agent 循环这一节把前面的代码串起来形成一个可以复现的验证路径。目标不是复刻 Rome 的全部功能而是验证三件事Key 从 TaoToken 来、模型调用走 Base URL、token 消耗有记录。步骤一拿 Key。从 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentrome-ts-key 进入控制台创建 Key复制后只放在本地.env。步骤二确认 Base URL。所有模型入口统一写https://taotoken.net/api不要在工具配置里拼接其他路径。步骤三跑最小 provider。使用第 2 节的TaoTokenProvider跑一次 smoke test确认返回内容和 usage。步骤四跑最小 agent 循环。使用第 3 节的withUsageLog循环 3 次把每一步输出写入 memory 数组。步骤五查看 token 账本。读取.taotoken/usage.jsonl确认每次调用都有记录。wc -l .taotoken/usage.jsonl tail -n 3 .taotoken/usage.jsonl步骤六接入 Claude Code 或 Codex。用第 4 节的配置切换供应商先用短 prompt 验证再跑真实任务。下面是一个更接近 Rome 递归思路的 TypeScript 片段每轮把模型输出追加到 memory下一轮把 memory 摘要作为上下文。注意这里没有直接连接任何外部生产系统工具执行由本地函数模拟。// src/agent/recursive-loop.ts import dotenv/config; import { TaoTokenProvider } from ../model/taotoken-provider; import { withUsageLog } from ../model/with-usage-log; type MemoryItem { step: number; content: string }; function summarize(memory: MemoryItem[]): string { return memory .slice(-5) .map((m) 第${m.step}步${m.content.slice(0, 120)}) .join(\n); } async function main() { const provider withUsageLog(new TaoTokenProvider(), { filePath: .taotoken/usage.jsonl, traceId: recursive_${Date.now()}, scene: recursive_agent, }); const memory: MemoryItem[] []; for (let step 1; step 4; step) { const context summarize(memory); const res await provider.chat({ messages: [ { role: system, content: 你是一个递归 agent。根据已有记忆输出下一步最小可执行动作不要输出多余解释。, }, { role: user, content: context ? 已有记忆\n${context}\n请给出第 ${step} 步动作。 : 还没有记忆请给出第 ${step} 步动作。, }, ], }); memory.push({ step, content: res.content }); console.log([step ${step}] ${res.content}); console.log([usage] ${res.usage.totalTokens} tokens, model${res.usage.model}); } console.log(\n最终记忆条数, memory.length); } main().catch((err) { console.error(err); process.exit(1); });这个循环看起来简单但它已经具备 Rome 模型层的几个关键特征模型调用被 provider 封装、Key 来自环境变量、每次调用有 usage 记录、memory 在轮次之间传递。你可以继续把 memory 换成向量库或文件持久化把 provider 换成多模型路由把 tool 执行抽成独立模块。只要模型层不散落 Key后面的演化就不会失控。7. 边界与结论Rome 还早期但模型 Key 可以先管对Rome 仍然是一个非常早期的项目它的「agent OS」愿景很大从产品文档到工程落地还有很长的距离。它适合想研究 agent 架构、想自己搭 agent 底座的开发者不适合期待开箱即用的普通用户。但这不影响我们从它的 TypeScript 实现和模型层设计里提取有价值的部分把 Key 管起来把 provider 抽象出来把 token 消耗记下来。TaoToken 在这个工作流里的角色是模型访问入口。官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentrome-ts-end 可以创建和管理 KeyBase URL 固定为 https://taotoken.net/api代码里只保留 YOUR_API_KEY 占位符。无论你是在写 Rome 风格的递归 agent还是给 Claude Code、Codex、CC Switch 换供应商这套方法都能复用。可复现产出也很明确一份 TypeScript provider 代码、一份usage.jsonltoken 消耗记录、一份 Claude Codesettings.json和一份 Codexconfig.toml。先跑通最小循环再逐步加入记忆、工具和评估。不要一上来就追求完整的 agent OS先把模型层做对。如果你还没有 Key可以按下面路径走先通过模型对话验证模型是否可用再选择适合高频调用的 Coding Plan然后到控制台创建 API Key最后按 Claude Code 文档完成本地配置。顺序如下模型对话https://taotoken.net/models/detail/chat?utm_sourcetaotoken_aicg_blog_endutm_contentrome-ts-chatCoding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentrome-ts-plan创建 API Keyhttps://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentrome-ts-keysClaude Code 文档https://taotoken.net/doc/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_contentrome-ts-cc-doc把 Key 管好把模型入口固定把 token 账本留下剩下的 agent 复利才有讨论基础。