从零构建 Agent(2):选择并调用不同模型服务

系列:从零构建 Agent(总览) · 上一篇:定义一次模型调用 · 下一篇:接入阿里云百炼(待更新)

上一章固定了 models.complete(model, context) 的调用方式。本章增加第二个能力:切换模型时,上层代码不需要出现 Anthropic、OpenAI 等服务分支。

pi 把一个模型服务的模型目录、认证规则和调用能力封装成 Provider。本文第一次出现 Provider 时,只需要把它理解为"某个模型服务的接入对象"。

总览:一次调用如何找到模型服务

graph LR App[调用方] -->|调用 models.complete| Models[Models</br>查找 Provider 并应用认证] Models --> Provider[Provider</br>执行调用] Provider --> Vendor[模型服务] Vendor --> Reply[AssistantMessage]

调用方仍然只提交 ModelContext。Provider 的查找和认证处理都发生在统一入口内部。

1. 为什么不能在调用方判断服务方

最直接的实现是写条件分支:

ts 复制代码
if (model.provider === "anthropic") {
	return callAnthropic(model, context);
}
return callOpenAI(model, context);

如果每个调用方都自己判断服务方,新增一个服务时就要修改所有调用位置。

pi 把选择逻辑集中到 Models:它根据 model.provider 查找并调用对应的 Provider。新增服务时只需要注册新的 Provider,调用方仍然使用 models.complete(model, context),不需要增加服务方分支。

2. model.provider 指定模型服务

上一章只使用了 Model.id。为了选择模型服务,本章再使用真实 Model 类型中的 provider 字段:

ts 复制代码
interface Model {
	id: string;
	provider: ProviderId;
}

interface Provider {
	readonly id: string;
}

这是从两个真实类型中裁剪出的本章视图。model.provider 是服务标识,例如 anthropicopenai;它必须与注册表中的 provider.id 相同。

3. Models 如何完成路由

Models.complete() 最终使用下面这条内部调用路径:

text 复制代码
complete() → stream() → requireProvider() → applyAuth() → provider.stream() → result()

Models.stream() 是实际启动模型调用的统一入口。它返回一条可以持续接收模型输出的事件流;complete() 复用这个入口,并通过 result() 等待完整回复。

requireProvider() 负责按 model.provider 查找服务,找不到时立即报错:

ts 复制代码
private requireProvider(model: Model<Api>): Provider {
	const provider = this.providers.get(model.provider);
	if (!provider) {
		throw new ModelsError("provider", `Unknown provider: ${model.provider}`);
	}
	return provider;
}

找到 Provider 后,Models.stream() 应用该 Provider 的认证信息,再把调用交给它:

ts 复制代码
stream(
	model: Model<Api>,
	context: Context,
	options?: ModelsApiStreamOptions<Api>,
): AssistantMessageEventStream {
	return lazyStream(model, async () => {
		const provider = this.requireProvider(model);
		const { requestModel, requestOptions } = await this.applyAuth(model, options);
		return provider.stream(requestModel, context, requestOptions);
	});
}

执行流程仍然是三步:

  1. requireProvider() 根据 model.provider 查找 Provider;
  2. applyAuth() 应用该 Provider 的认证信息;
  3. provider.stream() 使用处理后的参数执行调用。

本节新出现的变量含义如下:

  • options:调用方传入的可选请求配置,没有传入时为 undefined
  • requestModel:应用认证配置后,本次请求实际使用的模型;
  • requestOptions:合并调用方配置和认证信息后,本次请求实际使用的参数。

lazyStream() 让认证准备和 Provider 调用在返回事件流后继续执行,不改变上述路由顺序。

等待完整回复的源码只做了一层调用。省略泛型和类型标注后,方法主体如下:

ts 复制代码
async complete(model, context, options) {
	return this.stream(model, context, options).result();
}

因此,complete() 和内部的 stream() 不会各自维护一套路由逻辑。complete() 沿同一条路径调用 Provider,并等待完整消息。

4. 同一份 Context 如何切换服务

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

const anthropicReply = await models.complete(anthropicModel, context);
const openaiReply = await models.complete(openaiModel, context);

两次调用只更换 ModelModels 分别找到对应 Provider;调用方不需要知道各服务的认证和调用细节。

5. 运行本章示例

完整程序在 labs/02-provider-routing.ts。它注册两个教学用 Provider,并通过同一个 models.complete() 入口调用它们:

bash 复制代码
node labs/02-provider-routing.ts

预期输出:

text 复制代码
anthropic: hello
openai: hello
chapter 2 example passed

示例只验证 model.provider 如何选择 Provider。Provider 返回固定消息,因此运行时不需要密钥或网络连接。

6. 本章小结

  • 调用方只使用统一的 models.complete(model, context)
  • Models 根据 model.provider 找到模型服务;
  • Provider 提供认证规则,Models 把认证结果应用到本次请求;
  • 服务选择和认证差异不会进入上层调用代码。

对应源码:models.tstypes.ts

相关推荐
武子康2 小时前
同一份长文问两次,SGLang 怎样少算一遍
人工智能·llm·agent
王解3 小时前
AG-33_Grok Build vs Claude Code:两种 Code Agent 路线对比
agent
2601_962298673 小时前
AWS推ADOP:用AI Agent把数据工程从周压缩到小时
ai·自动化·agent·aws·数据工程
王解3 小时前
AG-31_Grok Build 开源解读:马斯克的命令行 Agent
系统架构·开源·agent
张彦峰ZYF3 小时前
从“记住对话”到“经营组织经验”:TencentDB Agent Memory 的团队级记忆架构、工程取舍与企业落地边界
人工智能·架构·llm·agent·skill·agent memory·tencentdb
米小虾13 小时前
你的 Agent 有 1000 万上下文,为什么第 50 轮就开始失忆?
人工智能·agent
武子康15 小时前
商业比较词进入 AI Overview:Semrush 60 万关键词研究能说明什么
人工智能·ai·架构·agent·claude·codex·semrush