从零构建 Agent(3):接入阿里云百炼

上一篇:选择并调用不同模型服务 · 系列导读:从一次模型调用到 Agent 内核

上一章已经能根据 model.provider 选择 Provider,但示例 Provider 只返回固定消息。本章把同一个调用入口接到阿里云百炼:读者将学会区分"选择哪家模型服务"和"使用哪种通信协议",并用 pi 现有的协议实现完成一次真实模型请求。

sequenceDiagram participant Caller as 调用方 participant Models participant Provider as Bailian Provider participant Adapter as openai-completions 适配器 participant API as 百炼 Chat Completions Caller->>Models: complete(Model, Context) Models->>Provider: 选择 Provider、注入认证 Provider->>Adapter: stream(Model, Context) Note over Adapter: Context → Chat Completions 请求 Adapter->>API: POST /chat/completions API-->>Adapter: SSE 数据块 Note over Adapter: SSE 数据块 → AssistantMessage Adapter-->>Provider: AssistantMessageEventStream Provider-->>Models: AssistantMessageEventStream Models-->>Caller: result() → AssistantMessage

去程中,适配器把统一输入转换成百炼请求;回程中,同一个适配器把百炼数据块还原为统一消息。Provider 只负责把选中的模型、认证信息和协议实现连接起来。

1. 协议适配器把统一调用翻译成厂商协议

Provider 决定请求发给哪家服务;协议适配决定双方如何交换数据。这是两个不同的选择:

text 复制代码
model.provider = "bailian"                       → Models 找到 Bailian Provider
model.api = "openai-completions"                 → 声明模型使用的通信协议
createProvider({ api: openAICompletionsApi() })   → 提供该协议的真实实现

这里的"协议适配"包含两个方向:

  1. 把 pi 的 Context 转换成 Chat Completions 请求体;
  2. 把服务端返回的 SSE 数据块累积成 pi 的 AssistantMessage。

可以把协议适配器理解为 Provider 与模型服务之间的转换层。Provider 把统一的 Model、Context 和请求参数交给它;它负责生成厂商请求、发送请求,再把厂商响应转换成统一事件。

为了让 Provider 能以相同方式调用不同协议,pi 把适配器的调用形式统一为:

text 复制代码
stream(Model, Context, 请求参数) → AssistantMessageEventStream

源码使用 ProviderStreams 表示这条规则。它固定了适配器的边界:输入始终是 pi 的模型与上下文,输出始终是 pi 的消息事件流。Provider 因此不需要知道不同厂商的 HTTP 请求和流式响应有什么差别。

openAICompletionsApi() 提供的就是 Chat Completions 协议对应的 stream() 实现。这个实现接收统一输入,完成前面所说的双向转换,再返回统一事件流;所以同一个适配器可以调用所有兼容 Chat Completions 的服务。

前两章已经使用过 model.provider,但没有展开 model.api。本章选择 openai-completions,是因为百炼提供与 Chat Completions 兼容的接口,而 pi 已经实现了这套请求和响应转换。

2. 用模型声明固定两级选择

这个 Provider 只调用 qwen3.8-flash,因此模型声明保持固定。下面是本章 Lab 使用的完整模型配置:

ts 复制代码
const model: Model<"openai-completions"> = {
	id: "qwen3.8-flash",
	name: "Qwen3.8 Flash",
	provider: "bailian",
	api: "openai-completions",
	baseUrl,
	reasoning: true,
	input: ["text", "image"],
	cost: { input: 0.8, output: 2.7, cacheRead: 0.1, cacheWrite: 1.25 },
	contextWindow: 1_000_000,
	maxTokens: 131_072,
	compat: {
		supportsStore: false,
		supportsDeveloperRole: false,
		supportsReasoningEffort: false,
		maxTokensField: "max_tokens",
		thinkingFormat: "qwen",
	},
};

provider 和 api 分别固定服务选择与协议选择。baseUrl 也必须保存在模型上,因为 pi 根据实际模型的地址创建请求客户端。

自定义 Provider 不会命中 pi 为其他厂商准备的自动兼容判断,所以 compat 明确关闭百炼文档没有定义的 store 和 developer 消息,并使用百炼支持的 max_tokens 字段。reasoning: true 记录模型支持思考模式;thinkingFormat: "qwen" 让适配器使用百炼的 enable_thinking 参数。本章调用没有传入 reasoningEffort,因此适配器发送 enable_thinking: false,仍然只获取普通文本回复。

3. 组装并注册 Bailian Provider

createProvider() 把地址、认证、模型目录和协议实现组合成一个 Provider:

ts 复制代码
const provider = createProvider({
	id: "bailian",
	name: "Alibaba Cloud Model Studio",
	baseUrl,
	auth: {
		apiKey: envApiKeyAuth("Bailian API key", ["DASHSCOPE_API_KEY"]),
	},
	models: [model],
	api: openAICompletionsApi(),
});

const models = createModels();
models.setProvider(provider);

envApiKeyAuth() 在请求发生时读取 DASHSCOPE_API_KEY。api: openAICompletionsApi() 把 Chat Completions 适配器绑定到这个 Provider。之后调用 provider.stream() 时,Provider 会把相同的 model、context 和请求参数交给适配器的 stream();Provider 本身不处理 HTTP 请求或 SSE 响应。

注册后,调用方仍然使用第一章的统一入口:

ts 复制代码
const reply = await models.complete(model, context, { maxTokens: 128 });

这一次调用在 pi 源码中的主路径是:

text 复制代码
Models.complete()
  → Models.stream()
  → requireProvider(model.provider)
  → applyAuth()
  → provider.stream()
  → 协议适配器.stream()
  → eventStream.result()

Models 先找到 bailian,Bailian Provider 再把调用交给创建时绑定的 openai-completions 协议实现;model.api 与这个实现使用同一个协议标识。最终的 result() 等待数据流结束并返回完整 AssistantMessage。

4. 适配器如何完成双向转换

发送请求前,buildParams() 调用 convertMessages(),把统一上下文转换成 Chat Completions 参数。省略与本章无关的选项后,核心结构如下:

ts 复制代码
const messages = convertMessages(model, context, compat);
const params = {
	model: model.id,
	messages,
	stream: true,
	stream_options: { include_usage: true },
};

例如下面的输入:

ts 复制代码
const context: Context = {
	systemPrompt: "Answer briefly.",
	messages: [{ role: "user", content: "hello", timestamp: Date.now() }],
};

会产生下面的核心请求体:

json 复制代码
{
  "model": "qwen3.8-flash",
  "messages": [
    { "role": "system", "content": "Answer briefly." },
    { "role": "user", "content": "hello" }
  ],
  "max_tokens": 128,
  "enable_thinking": false,
  "stream": true,
  "stream_options": { "include_usage": true }
}

timestamp 是 pi 的消息字段,不属于 Chat Completions 请求,因此转换后不会出现。

百炼以 SSE(服务端逐块返回的文本数据流)返回结果。适配器把每个 choice.delta.content 追加到同一个文本块,并把 finish_reason 转换成统一结束原因:

ts 复制代码
if (choice.delta.content) {
	const block = ensureTextBlock();
	block.text += choice.delta.content;
	stream.push({ type: "text_delta", delta: choice.delta.content, partial: output });
}

if (choice.finish_reason) {
	output.stopReason = mapStopReason(choice.finish_reason).stopReason;
}

因此,调用方拿到的不是百炼原始数据块,而是包含完整文本、结束原因和 token 用量的 AssistantMessage。

5. 运行一次真实请求

完整程序在 labs/03-bailian-provider.ts。在项目根目录的 .env 中填写:

dotenv 复制代码
DASHSCOPE_API_KEY=你的百炼_API_Key
BAILIAN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1

安装依赖并运行:

bash 复制代码
npm install
node labs/03-bailian-provider.ts

模型回复内容每次可能不同,成功输出的结构如下:

text 复制代码
qwen3.8-flash: <模型回复>
tokens: input=<输入 token>, output=<输出 token>, total=<总 token>
chapter 3 real call passed

Lab 直接调用 pi 的 models.complete()。它断言回复文本非空,并且数据流以 stop 或 length 结束;认证失败、模型不可用、网络错误或不完整的数据流都会使进程以错误退出。

6. 数据流必须有明确的结束原因

适配器收到流结束标记后,还会检查响应中是否出现过 finish_reason:

ts 复制代码
if (!hasFinishReason) {
	throw new Error("Stream ended without finish_reason");
}

这个检查保证 models.complete() 不会把意外中断的半条回复当成完整结果。它属于协议适配层,因为这一层负责判断 Chat Completions 响应是否完整。

7. 本章小结

本章接通了第一阶段的真实模型调用。读者现在可以:

  • 用 model.provider 选择百炼服务,用 model.api 选择通信协议;
  • 用 createProvider() 组合服务地址、认证、模型和现有协议实现;
  • 通过 models.complete() 提交统一上下文,得到统一的 AssistantMessage;
  • 运行 Lab 验证真实的百炼 HTTP/SSE 调用链。

对应源码:types.ts、models.ts、openai-completions.lazy.ts、openai-completions.ts。百炼请求参数和流式响应格式见阿里云百炼 Chat Completions 文档,模型能力与上下文限制见qwen3.8-flash 模型信息。

相关推荐
四六的六13 小时前
让 Agent 点界面,比补接口贵 30 倍:computer use 成本实测
人工智能·agent·个人开发·ai编程·ai产品·computer use·agent api
智能RPA1 天前
智能体自动化平台与主数据管理平台(MDM)对比评测
人工智能·自动化·agent·rpa
XLYcmy1 天前
AI 时代,MOM(制造运营管理系统)该如何演进? 下
ai·llm·agent·智能制造·数字孪生·mom·harness
Spcarrydoinb1 天前
【无标题】
ai·agent
Eric_见嘉1 天前
在职前端 Skill 和 MCP 分享
前端·后端·agent
Flynt1 天前
hindsight实测:给AI助理装上"海马体",我先跟root权限和2G内存缠斗了一下午
开源·agent
一直在努力的小宁1 天前
【阅读笔记】具身智能的真机数采,到了分水岭
人工智能·深度学习·机器学习·agent·具身智能·vlm·vln
效率工作实验室1 天前
AI 智能体平台和 AI Agent 平台是一回事吗?概念怎么区分?
ai·agent·平台·智能体平台·智能体与
北漂的尘埃1 天前
Hello-Agents——第一章 初识智能体
agent·hello-agent
七夜zippoe1 天前
WorkBuddy Agent + Blender 5.2 程序化建模实战:11 轮迭代建一台复古胶片单反,附棚拍伪影定位法
agent·blender·迭代·workbuddy·程序化建模