☰
AIRI 的 xsAI 实战:generateText、streamText 与 Tool Loop 实践指南
2026/10/16 6:03:34 网站建设 项目流程
  • 人工智能
  • AI Agent
  • MCP 服务
  • 工具调用
  • GUI 自动化
  • 语音
  • 数字人
  • 交互助手

【免费下载链接】airi

💖🧸 自托管、归你拥有的 Grok 风格 AI 伴侣与 waifu / 赛博生命灵魂容器,目标是接近 Neuro-sama 的高度;支持实时语音聊天、Minecraft 和 Factorio 游玩,支持 Web / macOS / Windows。

项目地址:https://gitcode.com/moeru-ai/airi
点击查看免费下载

本文围绕 AIRI 仓库中的 xsAI 参考文档 text-stream-tools.md 展开,系统讲解 xsAI 文本 API 的共用心智模型、generateText/streamText的返回结构与事件体系,以及基于stopWhen的工具循环(Tool Loop)。读完本文,你将掌握如何在 AIRI 这类项目中选型 xsAI 文本 API、消费流式事件,并落地一个轻量 Agent 循环,同时可以从 core-agent 的源码中看到这些概念在真实产品中的完整调用链。

一、共用心智模型:每次调用只需要四样东西

xsAI 的绝大多数文本 API 共享同一套调用骨架,参考文档将其总结为:

  • apiKey
  • baseURL
  • model
  • messages
  • 可选的fetch、headers、abortSignal

对于聊天类 API,还可以预期使用这些通用选项:

选项作用
temperature控制输出的随机性,越低越确定
topP核采样(nucleus sampling)参数
stop停止序列
seed随机种子
toolChoice工具选择指令(如'required')
tools工具定义列表
stopWhen控制多轮工具循环何时停止

其中temperature与topP的语义在 AIRI 源码中有明确注释:temperature越低输出越确定、聚焦,越高越有创造性;top_p为 0.1 时表示只考虑累积概率质量前 10% 的 token,见 StreamOptions 定义。

几个在 AIRI 中被反复强调的实践约束,来自 xsAI 技能文档 SKILL.md 的 Key constraints 一节:

  • baseURL和model在实际调用中几乎都是必需的,示例中应显式给出;
  • apiKey是否必需取决于提供方:托管服务通常需要,本地或代理端点可能不需要。Node.js 中优先用process.env,浏览器中优先读localStorage,不要硬编码密钥;
  • streamText()立即返回,调用方需要异步消费textStream、fullStream与结果 Promise。

从依赖锁定看,AIRI 通过 pnpm catalog 将 xsAI 生态统一钉在 0.5.1 版本,包括@xsai/generate-text、@xsai/stream-text、@xsai/tool、@xsai/shared-chat、@xsai-ext/responses等,见 pnpm-workspace.yaml。核心 Agent 运行时 core-agent 的依赖只引入了所需的细粒度包(@xsai/generate-text、@xsai/stream-text、@xsai/tool等),而不是伞形xsai包——这正是 xsAI “保持小体积”设计取向的直接体现。

二、generateText:一次调用拿到最终结果

当你只需要“一个最终结果”时,使用generateText。参考文档给出的结果形状包含:

  • text—— 最终文本
  • finishReason—— 结束原因
  • usage—— token 用量
  • messages—— 会话消息
  • steps—— 多步循环中的步骤
  • toolCalls/toolResults—— 工具调用与结果
  • reasoningText—— 推理文本

最简的调用形态(来自仓库内置的规范示例 recipes.md):

import { env } from 'node:process' import { generateText } from '@xsai/generate-text' const { text } = await generateText({ apiKey: env.OPENAI_API_KEY!, baseURL: 'https://api.openai.com/v1/', messages: [ { content: 'You are a helpful assistant.', role: 'system', }, { content: 'Write one sentence about the moon.', role: 'user', }, ], model: 'gpt-4o', })

该示例被明确定位为“简单脚本、测试、一次性辅助功能”的默认起点。

仓库中的真实用例:Provider 连通性探测

在 AIRI 里,generateText并不只用于生成对话内容。provider-inference包的 OpenAI 兼容校验器把它当作“最小真实请求”来探测用户配置的baseURL+apiKey+model是否可用,见 createOpenAICompatibleValidators:

await generateText({ apiKey: config.apiKey, baseURL: config.baseUrl!, headers: additionalHeaders, model: normalizedModel, messages: message.messages(message.user('ping')), // 部分 OpenAI 兼容服务方拒绝小于 16 的输出限制, // 因此探测统一使用 16 作为上限 ...(options?.chatCompletionTokenParameter === 'max_completion_tokens' ? { max_completion_tokens: 16 } : { max_tokens: 16 }), })

这里有两个值得注意的细节:

  1. 探测请求刻意压小输出(max_tokens: 16),注释说明部分 OpenAI 兼容服务方拒绝低于 16 的输出上限,16 是兼顾成本与兼容性的安全值;
  2. 错误分类:捕获到400状态码时校验器认为“连通成功但聊天失败”(可能是模型名错误),网络错误则判定连通失败,见 错误处理分支。

这正是文档推荐规则“优先用generateText写简单示例、脚本与测试”在工程上的延伸——测试与校验路径用同步等待的 API,主交互路径才走流式。

三、streamText:立即返回,事件驱动

streamText面向需要增量输出或工具事件的场景。它的关键行为是立即返回,并把结果拆成两类:

流(异步迭代器):

  • textStream—— 纯文本增量
  • fullStream—— 全量事件流
  • reasoningTextStream—— 推理增量

结果 Promise:

  • messages、steps、usage、totalUsage

fullStream中可能出现的事件类型包括:

  • text-delta
  • reasoning-delta
  • tool-call-streaming-start
  • tool-call-delta
  • tool-call
  • tool-result
  • finish
  • error

消费策略上,文档给出的建议是:只需要实时文本就用textStream;需要工具事件、推理增量或 finish 元数据时用fullStream。带工具的规范示例如下(recipes.md):

import { env } from 'node:process' import { streamText } from '@xsai/stream-text' import { stepCountAtLeast } from '@xsai/stream-text/shared-chat' import { tool } from '@xsai/tool' import * as v from 'valibot' const add = await tool({ description: 'Adds two numbers', execute: ({ a, b }) => (Number.parseInt(a) + Number.parseInt(b)).toString(), name: 'add', parameters: v.object({ a: v.pipe(v.string(), v.description('First number')), b: v.pipe(v.string(), v.description('Second number')), }), }) const { fullStream } = streamText({ apiKey: env.OPENAI_API_KEY!, baseURL: 'https://api.openai.com/v1/', messages: [ { content: 'You are a helpful assistant.', role: 'system', }, { content: 'What is 12 plus 30? Use the add tool.', role: 'user', }, ], model: 'gpt-4o', stopWhen: stepCountAtLeast(2), toolChoice: 'required', tools: [add], }) const text: string[] = [] for await (const event of fullStream) { if (event.type === 'text-delta') { text.push(event.text) } if (event.type === 'tool-call' || event.type === 'tool-result') { console.log(event) } } console.log(text.join(''))

AIRI 如何消费 streamText:streamChatCompletions

AIRI 的主聊天链路并没有直接for await迭代fullStream,而是通过onEvent回调消费 xsAI 的内部事件,再映射到自己的事件契约。核心实现在 streamChatCompletions:

const requestOptions: Parameters<typeof streamText>[0] = { ...input.config, prepareStep: ({ input: current, steps }) => { /* 见下一节 */ }, abortSignal: input.options?.abortSignal, temperature: input.options?.temperature, topP: input.options?.topP, messages, headers: mergeRequestHeaders(input.config.headers, input.options?.headers), streamOptions: { includeUsage: true }, stopWhen: stepCountAtLeast(10), tools: input.tools, toolChoice: input.options?.resolveStep ? undefined : input.options?.toolChoice, onEvent: async (event) => { const mapped = toAiriStreamEvent(event) if (mapped) await input.onEvent(mapped) }, } const result = streamText(requestOptions)

几个与文档一一对应、但只有看源码才能看到的细节:

  1. stopWhen: stepCountAtLeast(10):AIRI 把默认循环上限设为 10 步,stepCountAtLeast直接从@xsai/shared-chat导入,见 导入语句。
  2. streamOptions: { includeUsage: true }:显式要求提供方在流中回传用量,供上层totalUsagePromise 汇总。
  3. 事件映射层:toAiriStreamEvent 把 xsAI 事件(如text.delta、reasoning.delta、tool-call.done、tool-result.done、error)映射为 AIRI 自己的StreamEvent(text-delta、reasoning-delta、tool-call、tool-result、tool-error、error),并把与 UI 无关的text.start、step.done、tool-call.delta等直接过滤为null。该文件的注释还指出 xsAI 0.5 版本用isError: true标记失败的tool-result.done而不中断流,因此 AIRI 可以直接区分tool-error与tool-result。
  4. 结果 Promise 的等待时机:streamOnce 明确注释了steps在所有工具轮次结束后才 settle,而提供方的 finish 事件可能更早到达,因此统一以steps作为“整轮结束”的权威信号,并对usage、totalUsage、generatedTurn分别挂 rejection 兜底,避免 xsAI 产生未处理的 Promise rejection。

可以推断,这种“onEvent回调 + 显式等待steps”的组合,是为了让上层(语音播报、UI 渲染)能拿到逐 delta 的低延迟输出,同时仍有一个确定性的结算点。

四、Tool Loop:stopWhen、stepCountAtLeast 与 prepareStep

参考文档对工具循环的要点:

  • stopWhen以显式谓词开启重复工具循环;
  • 默认循环边界使用stepCountAtLeast(n);
  • 条件更复杂时可用and()、or()、not()组合;xsAI 还提供hasToolCall()这类内置谓词(见 SKILL.md);
  • 每一步会把 assistant 输出和工具结果追加进消息,再按需发起下一次 API 调用;
  • 当必须让模型调用工具时,使用toolChoice: 'required'。

上一节的add示例正是“toolChoice: 'required'+stepCountAtLeast(2)”的组合:第一轮强制模型调用add,第二轮拿到工具结果后生成答案。

AIRI 源码中的循环控制:prepareStep

xsAI 0.5 的每步钩子prepareStep只返回input、model、toolChoice三个字段。AIRI 利用这一点实现了“每步动态解析提供方配置”——包括每步重新读取角色设定、切换模型、注入 system prompt、以及在不支持 content 数组的提供方上强制把消息压平为字符串,见 prepareStep 实现。源码中的注释直接说明了这一约束的来源与解除条件:

xsAI 0.5 prepareStep returns only input, model, and toolChoice. It reads other options afterward but snapshots toolChoice beforehand.

也就是说,toolChoice在步骤切换前被快照,AIRI 只能通过在prepareStep中变更外层requestOptions对象(Object.assign替换apiKey、fetch、temperature、topP、tools等)来让后续步骤生效,并刻意把toolChoice置为undefined再按步回填。这是一个典型的“库能力边界 + 调用方适配”的工程案例。

跨协议工具循环:Responses 适配器

对于走 OpenAI Responses 协议的提供方,AIRI 使用@xsai-ext/responses扩展包的responses(),但循环控制语义与streamText完全一致:同样是stopWhen: stepCountAtLeast(10),同样用prepareStep逐步解析配置,见 streamResponses。两者的差异主要在事件面:Responses 适配器额外通过onNativeEvent消费原生事件,把web_search_call的状态变化转成 AIRI 的search事件、把output_text的 annotations 转成citations事件,见 onNativeEvent 处理。

值得注意的是,onNativeEvent与web_search工具类型并非@xsai-ext/responses上游默认提供的能力——AIRI 通过补丁 patches/@xsai-ext__responses@0.5.1.patch 给该包增加了WebSearchTool、WebSearchCall类型、onNativeEvent回调,并放宽了 error 事件的构造逻辑。补丁在 pnpm-workspace.yaml 中注册,这解释了为什么同一份“streamText + tools 的心智模型”在两条协议路径上都能成立。

循环之外的容错:瞬态重试与协议切换

工具循环真正跑在生产环境时还要处理两类问题,AIRI 的实现位于 llm-service.ts:

  1. 瞬态失败重试:408、429、5xx被视为可重试错误,退避序列为[3_000, 6_000, 12_000]ms,并尊重服务端的Retry-After(上限 30 秒);但仅在尚未向消费者发出任何流事件时才允许重试——因为一旦文本开始渲染/播报或工具已执行,重复请求会造成副作用重复,见 transientRetry 与 streamWithTransientRetry。
  2. 协议/提供方中途切换:prepareStep里如果发现下一步应走不同协议(如从 chat-completions 切到 responses),会抛出RequestSwitch,携带已完成的“部分轮次”;外层streamFrom把它接住,把已完成轮次并入会话后以新提供方继续,并设有 10 次切换、10 个轮次的硬上限防止死循环,见 streamFrom。

五、选型规则:什么时候用哪个 API

汇总参考文档的 Recommendation rules,并结合 AIRI 的实际分工:

场景推荐 APIAIRI 中的落点
简单示例、脚本、测试、一次性辅助generateTextProvider 连通性/能力探测(openai-compatible.ts)
UI、实时输出、Agent 式流程、事件驱动集成streamText主聊天链路(chat-completions.ts)
轻量 Agent 循环(不引入完整 Agent 框架)streamText+tools+stopWhencore-agent 的streamFrom整条编排
Responses 协议提供方@xsai-ext/responses的responses()responses.ts

其中“streamText+ 工具 = 轻量 Agent 循环”这一点在 AIRI 中体现得最为彻底:整个core-agent没有依赖任何重型 Agent 框架,一个助手轮次(AssistantTurn)由“N 次模型调用 + 工具执行 + 步骤投影”组成,循环边界由stepCountAtLeast(10)控制,事件面由toAiriStreamEvent统一收敛,用量由totalUsage汇总。

六、小结

xsAI 的文本 API 体系可以用一句话概括:同一个apiKey + baseURL + model + messages心智模型下,generateText负责“拿结果”,streamText负责“拿过程”,stopWhen负责“拿多少轮”。AIRI 仓库为这套模型提供了完整的工程注脚:

  • 校验与测试路径用generateText发最小探测请求(max_tokens: 16);
  • 主聊天路径用streamText的onEvent回调做低延迟事件下发,以stepsPromise 作为整轮结算点;
  • 工具循环统一以stepCountAtLeast为默认边界,配合toolChoice: 'required'强制首轮调用;
  • prepareStep的字段限制催生了每步变更请求配置的适配模式,并衍生出跨协议RequestSwitch与瞬态重试两类生产级容错。

如果你在 OpenAI 兼容端点之上构建自己的伴侣/助手类产品,可以直接复用仓库中的这套调用链作为参考实现:chat-completions.ts、responses.ts、xsai-events.ts 与 llm-service.ts 共同覆盖了从事件映射、循环控制到错误恢复的完整面。

  • 人工智能
  • AI Agent
  • MCP 服务
  • 工具调用
  • GUI 自动化
  • 语音
  • 数字人
  • 交互助手

【免费下载链接】airi

💖🧸 自托管、归你拥有的 Grok 风格 AI 伴侣与 waifu / 赛博生命灵魂容器,目标是接近 Neuro-sama 的高度;支持实时语音聊天、Minecraft 和 Factorio 游玩,支持 Web / macOS / Windows。

项目地址:https://gitcode.com/moeru-ai/airi
点击查看免费下载

相关推荐

上一篇:LinkSwift:九大网盘直链下载助手,告别限速实现高速下载自由
下一篇:你的手写笔记还在用纸笔?Obsidian手写插件让你告别纸质束缚

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

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

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

立即咨询