用 Chat Completions 客户端把 Microsoft Agent Framework 接到 Router One
Microsoft Agent Framework 是微软开源的 Agent 与工作流 SDK,支持 Python 和 .NET,也是 Semantic Kernel 与 AutoGen 的直接后继;两种语言都在 2026 年 4 月 2 日发布了 1.0。它的 OpenAI provider 可以连接任意 OpenAI 兼容端点,指向 Router One 后就能调用目录里在 /v1/chat/completions 上提供服务的所有聊天模型,每次请求都有成本和延迟 Trace;Agent 循环、函数工具、session、middleware 和工作流仍在你的进程里运行。有一个命名细节决定请求打到哪个端点:在 1.x 的 Python 包里,OpenAIChatCompletionClient 调用 Chat Completions,而 OpenAIChatClient(1.0 之前的代码用它表示 Chat Completions)现在调用的是 Responses API。本指南用 model、api_key 和 base_url 构造一个 OpenAIChatCompletionClient,运行带一个函数工具的 Agent,在同一个 session 上流式跑第二轮,给工具循环设上限,给出 .NET 里通过 OpenAIClientOptions.Endpoint 和 AsAIAgent 的等价写法,并列出 Semantic Kernel 与 AutoGen 里对应的设置。核对版本:agent-framework 1.19.0、agent-framework-openai 1.14.4、Microsoft.Agents.AI.OpenAI 1.22.0。
安装 agent-framework-openai 并设置凭证
使用 Python 3.10 或更新版本。OpenAI 客户端在 agent-framework-openai 包里,它会带上 agent-framework-core 和 openai Python 库;pip install agent-framework 则安装整套标准包。已正式发布的包不再需要 --pre。没装 provider 包就从 agent_framework.openai 导入客户端,会抛出点名 agent-framework-openai 的 ModuleNotFoundError。下面是 macOS/Linux 的 shell 示例,运行前替换两个占位符:一把 Router One Key,以及当前目录中某个聊天模型的精确 ID。ROUTER_ONE_* 是本示例自己定义并显式读取的变量名。参数省略时客户端有自己的回退变量:OPENAI_API_KEY、OPENAI_BASE_URL,模型则先读 OPENAI_CHAT_COMPLETION_MODEL,再读 OPENAI_MODEL。Agent Framework 不会自动加载 .env 文件;如果把这些值放在文件里,需要自己调用 load_dotenv() 或传入 env_file_path。运行 Python 文件时沿用同一环境。
python -m pip install agent-framework-openai export ROUTER_ONE_API_KEY="sk-your-router-one-key" export ROUTER_ONE_MODEL_ID="<exact-model-id-from-/models>"
把 Microsoft Agent Framework 配置到 Router One base URL
保存为 maf_router_one.py,再运行 python maf_router_one.py。OpenAIChatCompletionClient 的 model 原样填目录里的模型 ID,若 ID 自带 provider 前缀也要保留;base_url 填带 /v1 的 base URL,它会交给 openai 库的 AsyncOpenAI 客户端,客户端自行拼接 /chat/completions,所以一次运行里的每个模型来回都是一个 POST /v1/chat/completions;api_key 从 ROUTER_ONE_API_KEY 读取,显式传入还能保证即使 shell 里存在 AZURE_OPENAI_* 变量,客户端也走 OpenAI 路由。@tool 装饰器把带类型标注的 Python 函数变成函数工具:它的 JSON schema 放在请求的 tools 字段里,模型要求调用时,框架在你的进程里执行这个函数,并在下一次请求里把结果发回去。approval_mode 写出来是因为官方示例也这样写;never_require 是默认值,always_require 会在得到明确批准之前不执行该调用。function_invocation_configuration 给这个循环设上限,这里是每次运行最多 5 个模型来回、10 次工具执行。Agent(client=..., instructions=..., tools=[...]) 是当前的构造方式,client.as_agent(...) 构造的是同一个对象。agent.create_session() 返回 AgentSession;用这个客户端时,历史保存在你进程内的 session 里,每次运行都会作为 messages 重新发送。第一次运行返回 AgentResponse:text 是最终答案,usage_details 是 API 为这次运行的各个请求报告的输入、输出 token 之和。第二次运行传入 stream=True 并遍历 AgentResponseUpdate 分块;请求本身相同,只是带上 stream 和 stream_options include_usage。
import asyncio
import os
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.openai import OpenAIChatCompletionClient
@tool(approval_mode="never_require")
def get_order_status(order_id: Annotated[str, "The order number to look up."]) -> str:
"""Look up the shipping status of an order."""
return f"Order {order_id} shipped yesterday and arrives on Friday."
async def main() -> None:
# Chat Completions client: every model round trip is one POST /v1/chat/completions
client = OpenAIChatCompletionClient(
model=os.environ["ROUTER_ONE_MODEL_ID"],
api_key=os.environ["ROUTER_ONE_API_KEY"],
base_url="https://api.router.one/v1",
)
# Bound the tool loop (defaults: max_iterations=40, max_function_calls=None)
client.function_invocation_configuration.update({"max_iterations": 5, "max_function_calls": 10})
agent = Agent(
client=client,
name="SupportAgent",
instructions="You answer order questions. Call the tool when you need order data.",
tools=[get_order_status],
)
session = agent.create_session() # history stays in this process
# 1. Non-streaming run: text is the answer, usage_details sums the reported tokens
result = await agent.run("Where is order A-1042?", session=session)
print(result.text)
print(result.usage_details)
# 2. Same session, streamed: same request with stream=true
async for update in agent.run("When does it arrive?", session=session, stream=True):
if update.text:
print(update.text, end="", flush=True)
print()
if __name__ == "__main__":
asyncio.run(main())Agent Framework 的哪个设置发出哪种请求
端点由客户端类决定,每个请求带什么内容由 Agent 决定。下表按本指南涉及的设置,给出该填的值、它产生的请求,以及使用前要确认的事项。同一个包里有一个类不在本指南范围内:OpenAIEmbeddingClient 请求的是 /v1/embeddings,Router One 不提供该端点,所以 RAG 流程的 embedding 要留在其他服务商或本地模型上。
| Agent Framework 设置 | 填什么 / 产生的请求 | 怎么验证 |
|---|---|---|
| OpenAIChatCompletionClient → base_url(回退:OPENAI_BASE_URL) | https://api.router.one/v1 | 交给 openai 库的 AsyncOpenAI 客户端,由它在后面拼 /chat/completions;第一次运行就报 404 not_found,说明 /v1 少了或重复了 |
| OpenAIChatCompletionClient → model(回退:先 OPENAI_CHAT_COMPLETION_MODEL,再 OPENAI_MODEL) | 精确的目录 ID | Logs 里每条 Trace 的模型与 /models 逐字一致;关键字是 model,1.0 之前代码里的 model_id 会触发 TypeError |
| OpenAIChatCompletionClient → api_key(回退:OPENAI_API_KEY) | 为这个 Agent 单独创建、设了 maxSpend 的 Router One Key | 第一次请求出现在 Dashboard → Logs 里这把 Key 名下;显式传入 Key 时,即使设置了 AZURE_OPENAI_* 变量,客户端也走 OpenAI 路由 |
| 用同样三个参数构造 OpenAIChatClient(模型回退:OPENAI_CHAT_MODEL) | POST /v1/responses:在 1.x 的包里,这个类是 Responses API 客户端 | 只适用于详情页列出 /v1/responses 的 ID(当前在售的 GPT 系列与 DeepSeek ID),其他系列会返回 400 must be called via。带 session 时默认用 previous_response_id 续接,除非 options 里带 store=False,所以多轮对话要用一次真实请求确认 |
| Agent(tools=[...]) 配合 @tool 函数 | 带 tools 字段(JSON schema)的 Chat Completions;函数在你的进程里执行 | 模型详情页列出工具调用;get_web_search_tool() 加的是 web_search_options,属于服务商托管字段,依赖它之前先用一次真实请求确认 |
| client.function_invocation_configuration | max_iterations(默认 40)限制模型来回次数;max_function_calls 和 max_duration_seconds 默认为 None | max_iterations 用完后,还会再发一次 tool_choice 为 none 的请求来写出最终答案,所以不算重试时一次运行最多 max_iterations + 1 个请求 |
| agent.run(..., stream=True) | 同一个请求,改为 stream=true 并带 stream_options include_usage | 模型页的流式支持;中途断开的流记为 HTTP 499 client_cancelled |
| agent.create_session() | 历史由 InMemoryHistoryProvider 保存在 session 的 state 里,每次运行都作为 messages 重新发送 | 输入 token 逐轮增长;用 session.to_dict() 和 AgentSession.from_dict() 持久化。Chat Completions 没有服务端会话状态,session 就是这段历史的唯一副本 |
.NET:设置 OpenAIClientOptions.Endpoint 并调用 AsAIAgent
在 .NET 里,连接由官方 OpenAI 库负责,Agent Framework 在它之上加一层 Agent。安装 Microsoft.Agents.AI.OpenAI;Learn 页面的命令仍带 --prerelease,但 NuGet 上已有稳定的 1.x 版本(核对本指南时为 1.22.0),所以这个参数可以不加。OpenAIClientOptions.Endpoint 要填带 /v1 的 URL:该库的默认端点是 https://api.openai.com/v1,并且会自己拼接 /chat/completions,所以只填 https://api.router.one 会得到 404 not_found。GetChatClient(model) 选的是 Chat Completions,AsAIAgent 扩展方法把它包装成 ChatClientAgent;调用 CreateAIAgent 的代码早于 2026 年 1 月改名为 AsAIAgent 的那次变更。GetResponsesClient().AsAIAgent(model: ...) 是 Responses 版本,模型系列规则与 Python 的 OpenAIChatClient 相同。函数工具通过 tools: [AIFunctionFactory.Create(Method)] 传入,经由 Microsoft.Extensions.AI 的 FunctionInvokingChatClient 在你的进程里执行,它的 MaximumIterationsPerRequest 默认是 40。RunAsync 返回完整响应,RunStreamingAsync 以流式返回更新。
// dotnet add package Microsoft.Agents.AI.OpenAI
using System.ClientModel;
using Microsoft.Agents.AI;
using OpenAI;
using OpenAI.Chat;
var key = Environment.GetEnvironmentVariable("ROUTER_ONE_API_KEY")
?? throw new InvalidOperationException("ROUTER_ONE_API_KEY is not set.");
var model = Environment.GetEnvironmentVariable("ROUTER_ONE_MODEL_ID")
?? throw new InvalidOperationException("ROUTER_ONE_MODEL_ID is not set.");
AIAgent agent = new OpenAIClient(
new ApiKeyCredential(key),
new OpenAIClientOptions { Endpoint = new Uri("https://api.router.one/v1") })
.GetChatClient(model)
.AsAIAgent(instructions: "You answer order questions.", name: "SupportAgent");
Console.WriteLine(await agent.RunAsync("Where is order A-1042?"));给一次 Agent 运行定预算,并逐次核对请求
一次 agent.run() 是一个循环,不是一个请求。直接回答只有一个请求;每一批工具调用都会多一个来回,因为工具结果要在新的请求里发回模型,同时把到目前为止的对话重新发送,所以输入 token 每轮都在增长。max_iterations(默认 40)限制来回次数,用完之后框架还会再发一次 tool_choice 为 none 的请求来拿到最终答案;max_function_calls 和 max_duration_seconds 默认不设限,并且文档说明它们只在每一批并行工具调用结束后才检查,所以某一批可能超出限制。它们都不是花费上限。在框架之下,openai 库默认对连接错误和 408、409、429、5xx 响应重试 2 次。框架构造这个客户端时没有传 max_retries,所以要调整就传入自己的客户端:async_client=AsyncOpenAI(base_url=..., api_key=..., max_retries=0)。每一次真正到达网关的尝试在 Dashboard → Logs 里都是独立请求,有各自的 request_id、Trace 和费用。给每个 Agent 单独一把设了 maxSpend 的 Key:失控的循环会在你设的上限处收到 HTTP 402,openai 库不会重试 402,钱包和其他 Key 不受影响。用 WorkflowBuilder 搭的工作流同样在你的进程里运行,每个 Agent executor 的模型调用都是同一把 Key 上的又一个请求。Agent Framework 的 OpenTelemetry 层默认开启,但在你配置 exporter(例如调用 configure_otel_providers())之前不会导出任何数据;它会产生 invoke_agent、chat 和 execute_tool 三类 span,只有开启敏感数据记录(ENABLE_SENSITIVE_DATA,默认关闭)时才记录提示词。这些 span 是你应用自己的 trace,导出到哪里就留在哪里。result.usage_details 是框架对 API 报告用量的求和;费用以 Logs 记录为准。按 Key、时间、模型和 token 数对账,报障时保留 request_id。Router One 只记录模型调用元数据,不运行 Agent 循环,也不运行你的工具、middleware 或工作流。
Microsoft Agent Framework 该填哪个模型 ID?
从 /models 页复制精确的模型 ID,保留大小写、连字符和版本后缀,不要用展示名称代替。打开该模型的详情页,核对支持的 API 端点、上下文窗口和工具调用等能力,再与 Microsoft Agent Framework 当前选择的 provider 和功能对应。模型出现在目录里,不等于当前客户端能调用它的所有功能。建议为每个工具单独建 API Key,并设置 maxSpend 消费上限。
Microsoft Agent Framework 用的是哪种 API 协议?
OpenAI 兼容描述的是接口格式,不能据此推断 Chat Completions(/v1/chat/completions)、Responses(/v1/responses)和 Anthropic Messages(/v1/messages)可以互换。先核对工具当前版本、provider 配置和实际请求路径,再查模型详情与 API 兼容性事实页。一次普通对话成功,也不能证明服务端工具、历史状态或文件编辑功能都受支持。
在 trace 里验证 Microsoft Agent Framework 的调用
先在 Microsoft Agent Framework 发出一次简单文本请求,再到 Dashboard → Logs 按时间、模型和 request_id 核对 trace:tokens、花费、延迟和状态码。成功后再分别验证流式输出、工具调用和多轮历史。失败时保留实际请求路径、完整错误消息及 request_id;没有对应日志时,先查客户端配置和网络,不能仅凭客户端报错认定是网关或上游故障。
常见问题
第一次运行就抛出 ChatClientException: service failed to complete the prompt,里面包着的错误是什么意思?
客户端把 openai 库抛出的异常包了一层:消息以 service failed to complete the prompt: 结尾,后面跟着库自己的文本,例如 Error code: 404 和响应体,原始异常可以从 __cause__ 取到。看状态码。404 not_found 说明 base_url 少了 /v1 或写了两次:openai 库会在你传入的地址后面拼 /chat/completions,只填 https://api.router.one 会打到 /chat/completions,网关的消息会直接说明 base URL 必须以 /v1 结尾。401 AUTH_INVALID_API_KEY 是网关没有接受这把 Key,核对运行该文件的环境里 ROUTER_ONE_API_KEY 的值。400 且带 must be called via,说明模型 ID 被发到了不提供它的端点;在这个框架里,原因几乎总是代码构造的是 OpenAIChatClient:在 1.x 的包里这个类调用 /v1/responses,Chat Completions 对应的类是 OpenAIChatCompletionClient。还有两个错误在发出任何请求之前就会出现,类型都是 SettingNotFoundError。Model must be specified via the 'model' parameter or the 'OPENAI_CHAT_COMPLETION_MODEL', 'OPENAI_MODEL' environment variable 表示构造函数既没拿到参数,也没找到环境变量。Azure OpenAI client requires either an API key or an Azure AD token provider 表示没有找到 Key:既没有 api_key 也没有 OPENAI_API_KEY 时,构造函数会落到 Azure 路由分支,所以消息里提到 Azure,而真正要做的是提供 Router One Key。
我的代码导入了 OpenAIResponsesClient、ChatAgent 或 create_agent,或者传了 model_id。哪里变了?
这些都是预览阶段的名字,Microsoft Learn 上的 Python significant-changes 页面记录了这些改名。在 python-1.0.0rc6 里,OpenAI 客户端移到了 agent-framework-openai 包,OpenAIResponsesClient 改名为 OpenAIChatClient,原来的 OpenAIChatClient 改名为 OpenAIChatCompletionClient,所有地方的 model_id 改为 model,预览阶段的环境变量 OPENAI_CHAT_MODEL_ID 和 OPENAI_RESPONSES_MODEL_ID 换成了 OPENAI_CHAT_MODEL、OPENAI_CHAT_COMPLETION_MODEL 和 OPENAI_MODEL。更早的 beta 版本把 ChatAgent 改为 Agent(参数 chat_client= 改为 client=),create_agent 改为 as_agent,run_stream(...) 改为 run(..., stream=True),AgentThread 和 get_new_thread() 改为 AgentSession 和 create_session(),@ai_function 改为 @tool。Assistants 客户端在 python-1.0.0 中被移除,而 Router One 本来也不提供 Assistants API。对网关用户来说,容易踩的是第二条改名:1.0 之前写着 OpenAIChatClient(base_url=...) 的代码当时走 Chat Completions,现在走 /v1/responses,所以原来可用的 Claude、Gemini 或 Grok ID 在升级后会返回 400 must be called via。把类换成 OpenAIChatCompletionClient,把关键字换成 model 即可。
我从 Semantic Kernel 或 AutoGen 迁移过来,原来的 base URL 是在哪个设置里?
在 .NET 版 Semantic Kernel 里是 endpoint 参数:AddOpenAIChatCompletion(modelId: ..., apiKey: ..., endpoint: new Uri(...)),URI 填 https://api.router.one/v1,文档把它标为实验特性,需要 #pragma warning disable SKEXP0010;它最终设置的是上文同一个 OpenAI 库选项,所以 URL 要带 /v1。在 Python 版 Semantic Kernel 里,OpenAIChatCompletion 没有 base URL 参数,要传入一个客户端:OpenAIChatCompletion(ai_model_id=..., async_client=AsyncOpenAI(base_url=..., api_key=...))。在 AutoGen 里是 autogen_ext.models.openai 的 OpenAIChatCompletionClient(model=..., base_url=..., api_key=..., model_info={...}),参考文档写明:模型名不是有效的 OpenAI 模型时必须提供 model_info,带前缀的目录 ID 正属于这种情况。Agent Framework 里同名的类在 agent_framework.openai 下,不需要 model_info:model、api_key 和 base_url 就够了。有一个行为差异会影响预算:AutoGen 迁移指南指出,AssistantAgent 在不调高 max_tool_iterations 时只跑单轮,而 Agent Framework 的 Agent 会持续调用工具直到得出最终答案,所以把工具调用多的 Agent 迁过来之前,先设好 function_invocation_configuration 和按 Key 的 maxSpend。
Microsoft Agent Framework 能通过网关用哪些模型?
选用当前目录中同时支持 Microsoft Agent Framework 所用端点和所需功能的模型。精确 ID、当前单价与能力以 /models 及模型详情为准;不要仅按 GPT、Claude 等系列名判断兼容性。客户端能列出模型,只证明模型发现成功,仍需验证实际调用。
能列出模型,但调用报 400 或 404,怎么办?
先记录实际请求路径和错误消息,再核对精确模型 ID。400 可能是参数、工具类型或模型与端点不匹配;404 可能是请求路径或资源不存在,不能直接判定模型下线。若错误提示 must be called via,按它指明的端点调整客户端 provider,或换用支持当前端点的模型。不要在所有工具里统一增删 /v1 或 /chat/completions。
中国大陆能直连吗?
能。网关在大陆可直连、无需 VPN,配置与全球环境完全一致。
报 401/402/403/429 怎么排查?
先到 Dashboard → Logs 核对请求及错误消息。401 查 Key 是否传入和有效;402 查钱包余额与 maxSpend;403 查 Key 权限和访问限制;429 查请求频率、token 限额及上游限流,按错误来源处理。保留 request_id,再按错误码速查页逐项排查。