构建支持语音的 OSDK 应用
这是一篇动手教程:用 OSDK 把实时语音能力接进你自己的应用。注意 —— 录音与知情同意由你负责。
https://www.palantir.com/docs/foundry/realtime-audio/build-a-voice-enabled-osdk-application/
原始标题:Realtime audio • Build a voice-enabled OSDK application • Palantir · 所属:其他 AIP 应用
先记住这几条
写在前面
在部署任何录制或转录人类语音的应用之前,请确保已通知参与者,并在你所在司法管辖区要求的情况下备好同意流程。无论参与者是应用用户、通话中的第三方,还是任何其他语音被采集的人,这可能都适用。请参阅录音、转录与知情同意。
本教程将带你构建一个 OSDK 应用,它采集麦克风音频、将其流式传输到实时模型,并播放模型的语音回复。该应用以 Foundry 用户身份进行认证,因此每次交互都基于该用户的权限以及其可访问的 Ontology。关于 Foundry 上实时音频的高层概览,请参阅实时音频。
你将构建什么
到本教程结束时,你将拥有一个在浏览器中运行的 TypeScript 应用,它可以:
- 通过标准的 OSDK OAuth 流程对用户进行认证。
- 直接从浏览器打开一条到 Foundry 代理端点的 WebSocket 连接,该端点会转发到实时模型。
- 为智能体提供一个从 Ontology 读取数据的工具,并以用户的权限运行。
- 将麦克风音频流式传输到模型,并播放模型的语音回复。
前置条件
在开始之前,请确保你具备以下条件:
- 一个通过 @osdk/create-app 搭建好的 OSDK 应用。生成的应用包含本教程所依赖的 OAuth 脚手架。关于 OSDK 本身的完整参考,请参阅 Ontology SDK。
- 在 Developer Console 中注册的一个不受限自定义应用。OSDK 前端针对该应用进行认证。需要“不受限”是为了让访问令牌能够包含步骤 1中使用的
language-model-service:use-modelscope。 - 在你的 enrollment 中启用的一个实时模型。列表请参阅可用的音频模型。本教程使用
gpt-realtime-2。 - 在你的应用中安装的
@openai/agents包:
npm install @openai/agents第 1 步:获取 OAuth 访问令牌
由 @osdk/create-app 生成的 OSDK 应用在 src/client.ts 中包含一个 OAuth client。该 client 使用来自 @osdk/oauth 的 createPublicOauthClient 创建,并暴露一个用于获取当前访问令牌的方法。clientId、foundryUrl 和 redirectUrl 的值在运行时从 @osdk/create-app 根据你的 .env 配置注入到 index.html 中的 osdk-* meta 标签读取。
为 OAuth client 配置 language-model-service:use-model scope,使生成的访问令牌被允许连接到实时端点。没有这个 scope,实时端点会在认证期间拒绝该 WebSocket 连接。该 scope 仅在不受限自定义应用上可用——在继续之前请确认你的应用是不受限的。
下面的代码片段展示了添加实时音频 scope 之后 src/client.ts 的完整形态。该文件的大部分由 @osdk/create-app 生成;本教程唯一需要的改动是在 scopes 数组中包含 language-model-service:use-model。这些导出在教程的其他地方会被使用:auth 用于下方获取令牌,foundryUrl 用于在步骤 2中构建 WebSocket URL,client 用于步骤 3中的 OSDK 查询。
import { createClient, type Client } from "@osdk/client";
import { createPublicOauthClient, type PublicOauthClient } from "@osdk/oauth";
function getMetaTagContent(tagName: string): string {
const elements = document.querySelectorAll(`meta[name="${tagName}"]`);
const element = elements.item(elements.length - 1);
const value = element ? element.getAttribute("content") : null;
if (value == null || value === "") {
throw new Error(`Meta tag ${tagName} not found or empty`);
}
return value;
}
export const foundryUrl = getMetaTagContent("osdk-foundryUrl");
const clientId = getMetaTagContent("osdk-clientId");
const redirectUrl = getMetaTagContent("osdk-redirectUrl");
const ontologyRid = getMetaTagContent("osdk-ontologyRid");
const scopes = [
"language-model-service:use-model",
// Other scopes your application requires, for example:
// "api:read-data",
// "api:use-ontologies-read",
];
export const auth: PublicOauthClient = createPublicOauthClient(
clientId,
foundryUrl,
redirectUrl,
{ scopes }
);
export const client: Client = createClient(foundryUrl, ontologyRid, auth);
export default client;要在运行时获取访问令牌,请调用 auth.getTokenOrUndefined()。如果用户尚未登录,请先调用 auth.signIn() 然后重试:
import { auth } from "../client";
let token = await auth.getTokenOrUndefined();
if (!token) {
await auth.signIn();
token = await auth.getTokenOrUndefined();
}
if (!token) {
throw new Error("Unable to obtain access token. Please try again.");
}第 2 步:连接实时端点
实时端点在以下 URL 接受 WebSocket 连接,其中 <your-foundry-domain> 是你 Foundry 环境的主机名(与 src/client.ts 中用作 foundryUrl 的值相同),而 model 查询参数用于选择要使用的实时模型:
wss://<your-foundry-domain>/language-model-service/ws/v1/open-ai/realtime?model=gpt-realtime-2这是一个 Foundry 代理端点,类似于 REST LLM 供应商兼容 API,但通过 WebSocket 暴露。该代理会将会话转发到底层供应商(根据你的 enrollment,为 OpenAI Direct 或 Azure OpenAI),并保留供应商的原生实时协议。因此,你可以不加修改地使用 OpenAI 实时 SDK(@openai/agents/realtime)。
访问令牌作为 WebSocket 子协议值传递,因为浏览器无法在 WebSocket 连接上设置自定义的 Authorization 头。请将该子协议值格式化为 Bearer-<access-token>。
访问令牌作为 WebSocket 子协议值的一部分,在浏览器的开发者工具网络面板中可见。请将其视为凭据:不要记录它,不要在面向用户的字符串中暴露它,也不要将其传输到除你的 Foundry 端点之外的任何系统。访问令牌是短时效的;WebSocket 会话使用其建立时的令牌,因此长时间运行的会话可能需要在令牌过期时重新连接。
使用 OpenAI 实时 SDK 打开连接,并提供一个自定义的 createWebSocket 函数,用该子协议值构造浏览器的 WebSocket。完整的连接流程封装在一个名为 startVoiceSession 的 async 函数中,它在会话就绪后返回。该函数由三个小型具名组成部分构成,下面分别定义,并在本节末尾进行组合。将结果保存为例如 src/voice/session.ts。
首先从导入和常量开始:
import {
RealtimeAgent,
RealtimeSession,
OpenAIRealtimeWebSocket,
} from "@openai/agents/realtime";
import { foundryUrl } from "../client";
const PROXY_URL =
`wss://${new URL(foundryUrl).host}/language-model-service/ws/v1/open-ai/realtime` +
`?model=gpt-realtime-2`;
const CONNECT_TIMEOUT_MS = 10_000;定义 createTransport 来构造传输层。createWebSocket 回调用 bearer token 子协议构造浏览器的 WebSocket,并附加记录诊断信息的 error 和 close 处理器。useInsecureApiKey: true 标志会禁用 SDK 中仅针对浏览器的一项防护,该防护禁止使用非临时(non-ephemeral)的 OpenAI key。它在这里不适用,因为认证由 WebSocket 子协议处理,而不是由 OpenAI key 处理。
function createTransport(token: string): OpenAIRealtimeWebSocket {
return new OpenAIRealtimeWebSocket({
url: PROXY_URL,
useInsecureApiKey: true, // auth is via the subprotocol below, not OpenAI's apiKey
// eslint-disable-next-line @typescript-eslint/no-explicit-any
createWebSocket: async ({ url }: { url: string }): Promise<any> => {
const ws = new WebSocket(url, [`Bearer-${token}`]);
ws.addEventListener("error", () => {
console.error("WebSocket connection failed (check network tab for details)");
});
ws.addEventListener("close", (ev) => {
// 1000 = normal close, do not surface as an error.
if (ev.code !== 1000) {
console.error(`WebSocket closed: ${ev.code}${ev.reason ? ` (${ev.reason})` : ""}`);
}
});
return ws;
},
});
}定义 createSession 来构造会话,包含音频输入/输出格式、服务端语音活动检测(server-side voice activity detection),以及一个内联转录模型,以便在对话进行时对用户的语音进行转录:
function createSession(
agent: RealtimeAgent,
transport: OpenAIRealtimeWebSocket,
): RealtimeSession {
return new RealtimeSession(agent, {
transport,
model: "gpt-realtime-2",
tracingDisabled: true,
config: {
outputModalities: ["audio"],
audio: {
input: {
format: "pcm16",
transcription: { model: "whisper-1" },
turnDetection: {
type: "server_vad",
threshold: 0.8,
silenceDurationMs: 500,
prefixPaddingMs: 300,
},
},
output: { format: "pcm16" },
},
},
});
}定义 awaitSessionReady 来等待会话变得可用。调用 session.connect() 会在会话能够收发音频之前就返回,因此要等待 session.created,然后等待第一个 session.updated 事件。其后的 200 毫秒延迟给 SDK 留出稳定的时间;没有它,早期的音频分块可能会丢失。如果会话未在 CONNECT_TIMEOUT_MS 内达到该状态,promise 会以超时错误被拒绝。apiKey 字段未被使用,因为认证由 WebSocket 子协议处理;SDK 只是要求一个非空字符串:
function awaitSessionReady(session: RealtimeSession): Promise<RealtimeSession> {
return new Promise<RealtimeSession>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Connection timeout"));
}, CONNECT_TIMEOUT_MS);
let created = false;
let updatedCount = 0;
session.on("transport_event", (ev) => {
if (ev.type === "session.created") {
created = true;
}
if (ev.type === "session.updated" && created) {
updatedCount++;
if (updatedCount === 1) {
setTimeout(() => {
clearTimeout(timeout);
resolve(session);
}, 200);
}
}
});
session.connect({ apiKey: "unused" });
});
}最后,将三个组成部分组合为 startVoiceSession:
export async function startVoiceSession(
agent: RealtimeAgent,
token: string,
): Promise<RealtimeSession> {
const transport = createTransport(token);
const session = createSession(agent, transport);
return awaitSessionReady(session);
}一旦你从步骤 1 获得了访问令牌并有了智能体定义(见步骤 3),就调用 startVoiceSession(agent, token)。该函数会在会话准备好收发音频时返回。
第 3 步:给智能体一个读 Ontology 的工具
上面的示例搭建了一个通用助手。要让它有用,需要给智能体提供从 Ontology 读取和写入数据的工具。OpenAI 实时 SDK 支持工具调用:定义一个工具,将其注册到智能体上,当模型判断自己需要该信息时就会调用该工具的 execute 函数。
工具的 execute 函数在浏览器中运行,使用用户的 OAuth 令牌和 OSDK client。模型从不直接接触 Ontology——它只看到工具返回的内容。由你决定要暴露哪些数据、要应用哪些筛选条件,以及要写回什么。
下面的示例展示了一个工具桩(stub)。请将函数体替换为你针对自己 Ontology 的 OSDK 查询。关于 OSDK 查询语法和示例,请参阅 TypeScript OSDK。
import { RealtimeAgent, tool } from "@openai/agents/realtime";
import { z } from "zod";
import { client } from "../client";
const lookupProduct = tool({
name: "lookup_product",
description: "Look up a product by name. Use this when the user asks about a specific product.",
parameters: z.object({
name: z.string().describe("The product name to look up"),
}),
execute: async ({ name }) => {
// TODO: replace this stub with an OSDK query against your Ontology.
// For example, if your Ontology has a Product object type:
//
// const page = await client(Product)
// .where({ name: { $startsWith: name } })
// .fetchPage({ $pageSize: 5 });
// return JSON.stringify(page.data.map((p) => ({
// id: p.id, name: p.name, price: p.price, stock: p.stock,
// })));
//
return JSON.stringify({
id: "stub-1",
name,
price: 0,
stock: 0,
note: "Replace the tool body with an OSDK query against your Ontology.",
});
},
});
const agent = new RealtimeAgent({
name: "Assistant",
instructions:
"You help users find products. When the user asks about a product, call the lookup_product tool.",
tools: [lookupProduct],
});将这个智能体传入步骤 2 中的 startVoiceSession(agent, token)。当用户说话时,模型会决定是否调用该工具。工具的 execute 函数在浏览器中针对 OSDK client 运行,结果会被回传给模型以生成回复。
工具也可以写入 Ontology。要做到这一点,请在工具的 execute 函数中调用一个 action。Actions 会经过标准的 Foundry 权限和校验路径。
第 4 步:串流麦克风音频并播放回复
给定 startVoiceSession 返回的 RealtimeSession,接下来连接麦克风采集和音频播放。使用浏览器的 MediaDevices API 采集麦克风音频,将其转换为 24 kHz 的 16 位 PCM,并在各分块到达时通过 session.sendAudio(chunk) 将每个分块转发给会话。应用会将从会话接收到的音频分块排队以进行播放。
麦克风采集和播放队列的实现不在本教程范围内。用于接收音频的会话级连接方式大致如下:
const session = await startVoiceSession(agent, token);
// Surface mid-session errors from the SDK. WebSocket-level transport errors
// (such as a failure to establish the connection) are logged to the console
// from the handlers in `createTransport`; if the connection never establishes,
// `startVoiceSession` rejects after `CONNECT_TIMEOUT_MS`.
session.on("error", (event) => {
console.error("Realtime session error:", event.error);
});
// Play back audio from the model
session.on("audio", (event) => {
playbackQueue.enqueue(event.data); // your playback implementation
});
// Clear playback when the model is interrupted (for example, the user starts speaking)
session.on("audio_interrupted", () => {
playbackQueue.clear();
});
// Send mic audio to the model as 16-bit PCM chunks at 24 kHz arrive
startMicCapture((pcm16) => session.sendAudio(pcm16)); // your mic capture implementation
// Optional: prompt the agent to greet the user immediately rather than
// waiting for the user to speak first.
session.sendMessage("(Session started. Greet the user briefly.)");下一步
- Build out your application. 将会话接入你的前端,添加你的应用所需的工具,并在工具的
execute函数中调用 OSDK 查询和 actions 以与 Ontology 集成。 - Improve agent instructions. 关于为实时模型编写有效指令的指引,请参阅 OpenAI 实时提示词指南 ↗。
延伸阅读 · 相关页面
按主题横向跳转,不必顺着目录一篇篇读。
本组其他页面 · 其他 AIP 应用
同一主题下的相邻内容。
- AIP Evolve 总览AIP Evolve 负责编排成群的 AI FDE 智能体,用于持续改进 Foundry 里的 AI 系统 —— 从"一
- AIP Model Catalog 总览Model Catalog 是平台里所有模型资源的总目录:统一查看有哪些模型、各自状态如何、被谁在用。选型与治理都从这里
- 模型弃用与迁移模型供应商经常弃用模型,依赖它的工作流就会被打断。这一篇讲 Palantir 如何通知、如何用 Upgrade Assi
- Realtime audio 总览:语音交互音频是通过 Ontology 与平台交互的一种模态:对话前后从本体拉取上下文,实时模型边听边转写,可选地回话并触发工具调
- AIP Threads 总览AIP Threads 让你与文档、数据"对话":把资料放进来,围绕它提问、追问、推进工作,形成一条可持续的线索。
- AIP Threads 快速入门一个简单的工作流教程:上传文档 → 与文档交互 → 与 AIP Chatbot 交互。
常见问题速答 · FAQ
关于「构建支持语音的 OSDK 应用」,读者最常问的几个问题。