- 人工智能
- AI Agent
- MCP 服务
- 工具调用
- GUI 自动化
- 语音
- 数字人
- 交互助手
【免费下载链接】airi
💖🧸 自托管、归你拥有的 Grok 风格 AI 伴侣与 waifu / 赛博生命灵魂容器,目标是接近 Neuro-sama 的高度;支持实时语音聊天、Minecraft 和 Factorio 游玩,支持 Web / macOS / Windows。
本文围绕 AIRI 仓库中的 xsAI 参考文档 text-stream-tools.md 展开,系统讲解 xsAI 文本 API 的共用心智模型、generateText/streamText的返回结构与事件体系,以及基于stopWhen的工具循环(Tool Loop)。读完本文,你将掌握如何在 AIRI 这类项目中选型 xsAI 文本 API、消费流式事件,并落地一个轻量 Agent 循环,同时可以从 core-agent 的源码中看到这些概念在真实产品中的完整调用链。
一、共用心智模型:每次调用只需要四样东西
xsAI 的绝大多数文本 API 共享同一套调用骨架,参考文档将其总结为:
apiKeybaseURLmodelmessages- 可选的
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 }), })这里有两个值得注意的细节:
- 探测请求刻意压小输出(
max_tokens: 16),注释说明部分 OpenAI 兼容服务方拒绝低于 16 的输出上限,16 是兼顾成本与兼容性的安全值; - 错误分类:捕获到
400状态码时校验器认为“连通成功但聊天失败”(可能是模型名错误),网络错误则判定连通失败,见 错误处理分支。
这正是文档推荐规则“优先用generateText写简单示例、脚本与测试”在工程上的延伸——测试与校验路径用同步等待的 API,主交互路径才走流式。
三、streamText:立即返回,事件驱动
streamText面向需要增量输出或工具事件的场景。它的关键行为是立即返回,并把结果拆成两类:
流(异步迭代器):
textStream—— 纯文本增量fullStream—— 全量事件流reasoningTextStream—— 推理增量
结果 Promise:
messages、steps、usage、totalUsage
fullStream中可能出现的事件类型包括:
text-deltareasoning-deltatool-call-streaming-starttool-call-deltatool-calltool-resultfinisherror
消费策略上,文档给出的建议是:只需要实时文本就用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)几个与文档一一对应、但只有看源码才能看到的细节:
stopWhen: stepCountAtLeast(10):AIRI 把默认循环上限设为 10 步,stepCountAtLeast直接从@xsai/shared-chat导入,见 导入语句。streamOptions: { includeUsage: true }:显式要求提供方在流中回传用量,供上层totalUsagePromise 汇总。- 事件映射层: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。 - 结果 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:
- 瞬态失败重试:
408、429、5xx被视为可重试错误,退避序列为[3_000, 6_000, 12_000]ms,并尊重服务端的Retry-After(上限 30 秒);但仅在尚未向消费者发出任何流事件时才允许重试——因为一旦文本开始渲染/播报或工具已执行,重复请求会造成副作用重复,见 transientRetry 与 streamWithTransientRetry。 - 协议/提供方中途切换:
prepareStep里如果发现下一步应走不同协议(如从 chat-completions 切到 responses),会抛出RequestSwitch,携带已完成的“部分轮次”;外层streamFrom把它接住,把已完成轮次并入会话后以新提供方继续,并设有 10 次切换、10 个轮次的硬上限防止死循环,见 streamFrom。
五、选型规则:什么时候用哪个 API
汇总参考文档的 Recommendation rules,并结合 AIRI 的实际分工:
| 场景 | 推荐 API | AIRI 中的落点 |
|---|---|---|
| 简单示例、脚本、测试、一次性辅助 | generateText | Provider 连通性/能力探测(openai-compatible.ts) |
| UI、实时输出、Agent 式流程、事件驱动集成 | streamText | 主聊天链路(chat-completions.ts) |
| 轻量 Agent 循环(不引入完整 Agent 框架) | streamText+tools+stopWhen | core-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。
相关推荐
xsAI 文本流与工具循环:airi 仓库中 generateText、streamText 与 Tool Loop 实战参考
xsAI 文本流与工具循环:airi 仓库中 generateText、streamText 与 Tool Loop 实战参考 本篇基于 airi 仓库内 xs
人工智能AI 应用大模型AI Agent交互助手语音数字人后端前端桌面应用移动开发GUI 自动化MCP 服务AIRI 的 xsAI 包选型指南:细粒度 @xsai/* 包与 xsai 伞包如何取舍
AIRI 的 xsAI 包选型指南:细粒度 @xsai/ 包与 xsai 伞包如何取舍 AIRI 的 LLM、语音、工具调用等能力构建在 xsAI 这套 Ope
人工智能AI 应用大模型AI Agent交互助手语音数字人后端前端桌面应用移动开发GUI 自动化MCP 服务AI SDK 入门实战:使用 TypeScript 的 generateText 与 streamText 构建 AI 文本应用
AI SDK 入门实战:使用 TypeScript 的 generateText 与 streamText 构建 AI 文本应用 本文是一份基于 AI SDK(
人工智能大模型AI 应用AI Agent工具调用LLM 网关MCP Clients
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考