OpenClaw @openclaw/ai 最小消费者示例:一个隔离 LLM 运行时、内置 Provider 与流式补全
2026/9/10 8:34:00 网站建设 项目流程

OpenClaw @openclaw/ai 最小消费者示例:一个隔离 LLM 运行时、内置 Provider 与流式补全

【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 🦞项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw

本文基于 OpenClaw 仓库中 examples/ai-chat 的官方最小示例,讲解如何仅使用已发布的@openclaw/ai包(不依赖任何 OpenClaw 应用代码)创建隔离的 LLM 运行时、注册 8 个内置 API Provider,并以纯 Node.js ESM 脚本流式消费一次模型补全。读完后你将掌握该包的公开入口(createLlmRuntimeregisterBuiltInApiProviders)、模型对象各字段的含义、三种 Provider(Anthropic / OpenAI / Ollama)的实际运行命令,以及底层延迟注册与事件流机制的源码实现。

示例定位:为什么这个示例有价值

@openclaw/ai是 OpenClaw 对外发布的可复用模型适配包,提供 Provider 中立的契约、Provider 适配器与流式原语。包文档(packages/ai/README.md)明确了它的能力边界:

  • 支持隔离的运行时实例:导入该包不会在包级别全局注册任何 Provider;
  • Provider 中立的契约、校验、诊断与事件流可从包根及@openclaw/ai/event-stream@openclaw/ai/transports@openclaw/ai/validation等子路径导入;
  • Provider 凭据、模型目录、重试与故障转移属于应用层关注点,OpenClaw 应用在这个包之上提供这些策略;
  • 主机策略(自定义 fetch 防护、密钥脱敏、strict-tool 默认值、诊断日志)可通过configureAiTransportHost注入,默认值是不活跃的(inert)
  • @openclaw/ai/internal/*系列子路径仅供 OpenClaw 应用自身使用,无 semver 保证,外部不应依赖。

examples/ai-chat 就是这个包"最小外部消费者"的活体证明:它只依赖@openclaw/ai的公开面,不引入任何 OpenClaw 应用代码。package.json(examples/ai-chat/package.json)声明为私有 ESM 包:

{ "name": "@openclaw/example-ai-chat", "version": "0.0.0-private", "private": true, "description": "Minimal external-consumer example for @openclaw/ai", "type": "module", "scripts": { "start": "node index.mjs" }, "dependencies": { "@openclaw/ai": "workspace:*" } }

这里有一个关键的运行前提:在仓库内部,@openclaw/ai的 workspace 链接解析到该包的已构建dist产物——也就是 npm 消费者实际安装的同一份产物。因此必须先执行pnpm build,否则示例无法解析到包内容。

完整示例代码解析(index.mjs)

整个示例只有约 80 行(examples/ai-chat/index.mjs),结构分四段:模型表、参数解析、运行时创建、流式消费。

1. 模型声明表:三个 Provider 的完整配置

const MODELS = { anthropic: { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", api: "anthropic-messages", provider: "anthropic", baseUrl: "https://api.anthropic.com", reasoning: true, input: ["text"], cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, contextWindow: 200_000, maxTokens: 8192, }, openai: { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", api: "openai-responses", provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, input: ["text"], cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 }, contextWindow: 1_050_000, maxTokens: 128_000, }, // Local Ollama server; no API key required. ollama: { id: process.env.OLLAMA_MODEL || "llama3.2:latest", name: "Ollama", api: "openai-completions", provider: "ollama", baseUrl: "http://localhost:11434/v1", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32_000, maxTokens: 4096, }, };

模型对象的每个字段对应@openclaw/ai的模型契约:

字段作用示例中的取值要点
id/name模型标识与展示名Ollama 的idOLLAMA_MODEL环境变量覆盖,默认llama3.2:latest
api决定走哪个内置传输(transport)三个取值分别对应anthropic-messagesopenai-responsesopenai-completions三种注册好的协议适配器
providerProvider 标识anthropic/openai/ollama
baseUrlAPI 端点Ollama 指向本机http://localhost:11434/v1(OpenAI 兼容端点)
reasoning是否具备推理(extended thinking / reasoning tokens)能力Anthropic、OpenAI 为true,Ollama 为false
input支持的输入模态均为["text"]
cost每百万 token 的计价参数本地 Ollama 全为 0
contextWindow上下文窗口大小Ollama 32K,OpenAI 1.05M
maxTokens单次最大输出 token 数Ollama 4096,OpenAI 128K

注意一个设计细节:Ollama 通过api: "openai-completions"复用 OpenAI 兼容传输——本地模型服务器只要暴露 OpenAI 兼容的/v1接口,就无需单独适配。

2. 命令行参数解析

const args = process.argv.slice(2); const providerFlag = args.indexOf("--provider"); const provider = providerFlag === -1 ? "anthropic" : args[providerFlag + 1]; const prompt = args.filter((_, i) => i !== providerFlag && i !== providerFlag + 1).join(" ") || "Reply with one short sentence: what is @openclaw/ai?"; const model = MODELS[provider]; if (!model) { console.error(`Unknown provider "${provider}". Use one of: ${Object.keys(MODELS).join(", ")}`); process.exit(1); }
  • --provider缺省时默认anthropic
  • 提示词 = 除--provider及其取值外的所有剩余参数拼接(保留空格分隔),再整体作为单条 user 消息;
  • 无提示词时使用内置默认提示;
  • 未识别的 provider 直接打印可用列表并以退出码 1 终止。

3. 运行时创建与 Provider 注册:整个包最核心的两步

const runtime = createLlmRuntime(); registerBuiltInApiProviders(runtime.registry);

对照 packages/ai/src/stream.ts 的实现,createLlmRuntime会创建一个隔离的运行时

export function createLlmRuntime(registry: ApiRegistry = createApiRegistry()) { function resolveApiProvider(api: Api) { const provider = registry.getApiProvider(api); if (!provider) { throw new Error(`No API provider registered for api: ${api}`); } return provider; } // ... return { registry, stream, complete, streamSimple, completeSimple }; }

从源码结构看,运行时暴露了 5 个成员:

  • registry:Provider 注册表(ApiRegistry),模型请求按model.api在此解析出对应的stream/streamSimple函数;未注册则抛No API provider registered for api: ...
  • stream(model, context, options?):走完整Context契约的流式调用,返回AssistantMessageEventStreamContract
  • complete(...)stream(...).result()的便捷封装,等待流结束返回最终AssistantMessage
  • streamSimple(model, context, options?):走简化的SimpleStreamOptions(如{ messages }),示例使用的正是这一档;
  • completeSimple(...)streamSimple(...).result()的便捷封装。

registerBuiltInApiProviders实现见 packages/ai/src/providers/register-builtins.ts。它一次性注册8 个内置传输,与 README 描述一一对应:

api 标识对应协议说明
anthropic-messagesAnthropic Messages示例 anthropic 配置所用
openai-completionsOpenAI Chat Completions 兼容Ollama 走此传输
openai-responsesOpenAI Responses API示例 openai 配置所用
azure-openai-responsesAzure OpenAI
openai-chatgpt-responsesChatGPT (Codex) Responses
mistral-conversationsMistral Conversations
google-generative-aiGoogle
google-vertexVertex

注册采用惰性加载createLazyRegistration为每个 api 注册一个包装函数,调用时才动态import()对应 Provider 模块(如import("./anthropic.js")),且加载结果被缓存(streamsPromise ??= importModule().then(select))。因此"Provider SDK 模块在首次使用时才加载"——注册本身是零成本的,示例进程只加载了实际请求命中的那个适配器。同时createLazyStream保证调用方同步拿到一个流对象,底层模块加载完成后通过forwardStream把事件逐条转发进这个外层流;加载失败时则推送一条error事件并以零用量结果收尾(projectProviderError会统一错误投影)。

4. 流式消费:stdout 与 stderr 的分流

const stream = runtime.streamSimple( model, { messages: [{ role: "user", content: prompt, timestamp: Date.now() }] }, // Ollama ignores credentials but the OpenAI-compatible transport requires one. provider === "ollama" ? { apiKey: "ollama" } : undefined, ); for await (const event of stream) { if (event.type === "text_delta") { process.stdout.write(event.delta); } } const result = await stream.result(); process.stdout.write("\n"); if (result.stopReason === "error" || result.stopReason === "aborted") { console.error(`error: ${result.errorMessage ?? result.stopReason}`); process.exit(1); } const { input, output } = result.usage; console.error(`[${model.id}] stop=${result.stopReason} tokens in=${input} out=${output}`);

三个值得注意的点:

  1. 输出分流:文本增量(text_delta事件)逐块写到 stdout;停止原因与 token 用量走 stderr。这使得模型输出可以被管道消费,而诊断信息不污染管道内容;
  2. Ollama 的占位 key:Ollama 本身不需要凭据,但 OpenAI 兼容传输要求存在apiKey,所以传入字面量"ollama"占位;
  3. 结果收尾stream.result()在流结束后返回最终AssistantMessage,包含stopReasonerrorMessage(错误投影时)与usageinput/outputtoken 数及cost明细)。stopReasonerroraborted时以退出码 1 终止。

运行方式:完整命令清单

POSIX Shell(Ollama 一条在 PowerShell 中同样可用)

ANTHROPIC_API_KEY=example-anthropic-key-not-real node index.mjs "Say hello" OPENAI_API_KEY=example-openai-key-not-real node index.mjs --provider openai "Say hello" # keyless, against a local Ollama server (OLLAMA_MODEL overrides the model id) node index.mjs --provider ollama "Say hello"

PowerShell

$env:ANTHROPIC_API_KEY = "example-anthropic-key-not-real" node index.mjs "Say hello" $env:OPENAI_API_KEY = "example-openai-key-not-real" node index.mjs --provider openai "Say hello" # Clear the session-scoped keys. $env:ANTHROPIC_API_KEY = $null $env:OPENAI_API_KEY = $null

两种写法的差异与限制需要注意:

  • POSIX 中KEY=... node index.mjs的形式,环境变量只作用于单条命令,执行完即消失;
  • PowerShell 的$env:赋值在会话内持续存在,直到手动清空(置$null)或关闭 shell——README 特意提示了这一点,避免密钥残留;
  • Ollama 分支完全无 key,但要求本机有一个运行中的 Ollama 服务器(http://localhost:11434),可用OLLAMA_MODEL环境变量覆盖默认的llama3.2:latest模型 id。

运行前请确认已执行pnpm build(workspace 链接指向构建产物),并将占位 key 替换为真实 API key。

面向库使用者的四条要点

README 末尾给出的四条 library consumer notes,逐条对应到源码事实:

  1. createLlmRuntime()给你隔离注册表:导入包本身不产生任何全局注册,每个createLlmRuntime()实例拥有独立的ApiRegistry(packages/ai/src/stream.ts 中默认参数createApiRegistry()即为每实例新建)。
  2. registerBuiltInApiProviders(runtime.registry)一次性获得 8 个内置传输:Anthropic、OpenAI Completions/Responses、Azure OpenAI、ChatGPT Responses、Google、Vertex、Mistral;Provider SDK 模块在首次使用时惰性加载(见上文createLazyRegistration分析)。
  3. 主机策略可注入:自定义 fetch、密钥脱敏、strict-tool 默认值、诊断日志通过configureAiTransportHost注入,默认实现是 inert 的——也就是说开箱即用时不会拦截或改写任何请求行为。
  4. 完整 TypeScript 类型随包发布:类型声明位于dist/*.d.mts(见 packages/ai/package.json 的exports字段,根及 7 个公共子路径均带types条件);示例刻意使用纯 ESM 以便在裸node环境下直接运行,无需 TS 工具链。

延伸阅读

  • 包文档:packages/ai/README.md
  • 运行时实现:packages/ai/src/stream.ts
  • 内置 Provider 注册与惰性加载:packages/ai/src/providers/register-builtins.ts
  • 示例源码:examples/ai-chat/index.mjs

这个示例展示了@openclaw/ai作为独立库的最小接入形态:隔离运行时 + 内置注册 + 一次流式补全。如果你的应用需要多 Provider 故障转移、凭据管理或模型目录,则应在该包之上自行实现这些应用层策略——这正是@openclaw/ai刻意不内置的边界。

【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 🦞项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw

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

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

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

立即咨询