LangChain 托管与 Agent Framework:微软 AI Agent 入门课第 14 课

2026-08-18

翻到 ai-agents-for-beginners 第 14 课的时候,很多人会愣一下:14-microsoft-agent-framework/code-samples/ 这一个目录里,既有 14-sequential.ipynb14-human-loop.ipynb14-middleware.ipynb 这些用 Microsoft Agent Framework 原生 API 写的 notebook,又躺着一个 14-langchain-hosted-agent.py——一个把 LangGraph 图挂到 Microsoft Foundry 上托管的脚本。两段代码长得完全不像,课程 README 也没有把它们摆在一起比。

这篇就干这件事:只对照仓库里写明的机制,看两条路各自把边界划在哪,以及这个差异什么时候会咬到你。

入口长什么样

原生这条路的起点是一个 provider。14-sequential.ipynb 里是这么配的:

provider = FoundryChatClient(
    project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
    model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
    credential=AzureCliCredential(),
)

FoundryChatClient 来自 agent_framework.foundry。有了它之后,agent 是从 provider 上长出来的——provider.as_agent(name=..., instructions=...)14-human-loop.ipynb 里进一步给它挂了工具和结构化输出:chat_client.as_agent(instructions=..., tools=[hotel_booking], default_options={"response_format": BookingCheckResult}),再包进 AgentExecutor(..., id="availability_agent") 当作工作流的一个节点。工具本身用 @tool 装饰器声明,14-middleware.ipynb 里是 @tool(description="Check hotel room availability for a destination city")

托管这条路的起点是一个 HTTP 服务。14-langchain-hosted-agent.pymain() 只有三行有效逻辑:

graph = create_agent(build_chat_model(), tools=[])
port = int(os.environ.get("PORT", "8088"))
ResponsesHostServer(graph).run(port=port)

create_agent 来自 langchain.agentsResponsesHostServer 来自 langchain_azure_ai.agents.hosting8088 是仓库当前代码里的默认端口,随版本可能变动。

真正值得停一下的是 build_chat_model():它先用 AIProjectClient(endpoint=project_endpoint, credential=credential) 拿到项目客户端,再调 project.get_openai_client() 取出兼容 OpenAI 的 base_url,最后把这个地址塞进 ChatOpenAI。注意最后一个参数:

token_provider = get_bearer_token_provider(credential, _AZURE_AI_SCOPE)

return ChatOpenAI(
    model=deployment,
    base_url=str(openai_client.base_url),
    api_key=token_provider,
)

api_key 收的不是一个字符串密钥,而是 get_bearer_token_provider 返回的 provider,scope 在文件里写死为 _AZURE_AI_SCOPE = "https://ai.azure.com/.default"。第一次照着改的时候很容易把它当成普通 key 字段填一个字符串。

第一个会咬人的差异:环境变量名不是一套

原生 notebook 读的是 AZURE_AI_PROJECT_ENDPOINTAZURE_AI_MODEL_DEPLOYMENT_NAME14-langchain-hosted-agent.py 的 docstring 与 README 写的却是 FOUNDRY_PROJECT_ENDPOINTFOUNDRY_MODEL_NAME。同一课、同一个目录,两套名字。

而且两者的取法不对称:endpoint 用的是 os.environ["FOUNDRY_PROJECT_ENDPOINT"] 这种直接下标,没有兜底;model 用的是 os.environ.get("FOUNDRY_MODEL_NAME", "gpt-5-mini"),带默认值。gpt-5-mini 是仓库当前代码里的默认值,随版本可能变动,docstring 里把它和 gpt-5-nano 一并列为「支持 Responses API 的已部署聊天模型」的举例。

README 还补了一句:当 agent 真的作为 hosted agent 跑在 Foundry 里时,平台会自动注入 FOUNDRY_PROJECT_ENDPOINT。所以本地那份 .env 是给你自己跑用的,别指望它跟前面几课的配置能直接复用。

认证类也不同:原生 notebook 用 AzureCliCredential(),托管示例用 DefaultAzureCredential()。两边的前置条件都写了要先 az login。至于为什么各挑一个,仓库里没有解释,我们也不替作者解释。

编排归谁管

这是两条路最实的分野。

原生这边,编排是框架的一部分。14-sequential.ipynb 里把两个 agent 串起来是这样写的:

workflow = (
    WorkflowBuilder(
        start_executor=front_desk_agent,
        output_executors=[front_desk_agent, concierge_agent],
    )
    .add_edge(front_desk_agent, concierge_agent)
    .build()
)

跑完之后用 events.get_outputs() 取结果,返回的是与 output_executors 顺序对应的一组响应对象。README 在 Workflows 一节里列了边的几种形态:Direct、Conditional、Switch-case、Fan-out、Fan-in;还列了内置事件,WorkflowStartedEventWorkflowOutputEventWorkflowErrorEventExecutorInvokeEventExecutorCompleteEventRequestInfoEvent

托管这边,README 的表述是 Foundry 管 runtime、会话、扩缩、身份与协议端点,而「你的 agent 逻辑留在 LangGraph 里」。也就是说编排还在你自己的图里,ResponsesHostServer 拿到的是一个已经编译好的 graph。仓库没有为这条路提供边类型或事件类型的对应物——不是说没有,是这门课里我们没有找到对应说明,所以不比

人工介入:两边都能停,但接的东西不一样

原生用的是 RequestInfoExecutor14-human-loop.ipynb 把它讲得很清楚:这个 executor 暂停工作流并发出 RequestInfoEvent,请求负载要自己定义成 RequestInfoMessage 的 dataclass 子类(notebook 里叫 HumanFeedbackRequest)。notebook 里有一句提醒特别值得抄下来——RequestInfoExecutor 本身收集输入,它只负责暂停;监听事件、收集人类输入这一段得你的应用代码自己写。

托管这边,README 写的是:如果你的图用了 LangGraph 的 interrupt()ResponsesHostServer 会把挂起的 interrupt 表达成 Responses 协议里的 function_callmcp_approval_request item,客户端用对应的 function_call_output / mcp_approval_response 恢复。

落点在这里:两条路都不会帮你把人接进来,你都得写一段「接住暂停」的代码。区别是接的东西所在的层不同——一个是进程内的事件对象,一个是 HTTP 请求体里的 item。如果你的人工审批环节本来就在另一个系统里,走协议那层反而更顺手;如果审批逻辑就在同一个 Python 进程里,事件那层少一次序列化。

会话状态

原生这边讲的是 agent thread:README 的 Agent Threads 一节给了 agent.get_new_thread()await thread.serialize()await agent.deserialize_thread(serialized_thread) 这一组。注意它挂在 agent 上,不是挂在 workflow 上,别混着说。

托管这边,README 说客户端靠传 previous_response_id 或一个 conversation ID 来续上;如果图在编译时带了 LangGraph 的 checkpointer,Foundry 会把会话状态挂到 checkpoint 上。README 自述:生产环境用持久化的 checkpointer,MemorySaver 适合本地测试。

安装与本地起服务,Windows 侧注意

托管路线的安装命令,README 里原样是:

pip install -U "langchain-azure-ai[hosting]>=1.2.4" azure-identity

那个版本下限是仓库文档当前写死的,随版本变动。README 写明 hosting extra 会装上两个协议库:azure-ai-agentserver-responses(提供兼容 OpenAI 的 /responses 端点)与 azure-ai-agentserver-invocations(提供通用的 /invocations 端点)。对应的协议选择,README 给了一张表:ResponsesHostServer/responses,用在需要 OpenAI 兼容的对话、流式、响应历史和会话线程的场景;InvocationsHostServer/invocations,用在需要自定义 JSON 形状、webhook 式端点或非对话式处理的场景。README 说 Responses 是 Foundry 里 agent 式开发的首选 API,多数 agent 从 ResponsesHostServer 起步。

本地起服务后的验证请求,文件 docstring 里原样是:

curl -sS -H "Content-Type: application/json" \
  -X POST http://localhost:8088/responses \
  -d '{"input":"Give me one tip for testing hosted agents.","stream":false}'

这里有个平台差异得自己处理:仓库给的环境变量设置用的是 export,上面这条 curl 用反斜杠续行,都是 Linux/macOS 的 shell 写法。Windows 的 PowerShell 里反斜杠不是续行符,环境变量也不是 export 那套语法,得自己改写成一行或换成 PowerShell 的写法——这是通用的 shell 差异,不是仓库内容。az login 两边都一样。

部署侧 README 给的是 Azure Developer CLI 的路径:azd ext install azure.ai.agentsazd ai agent init -m <manifest>azd ai agent run(本地跑容器,需要 Docker)、azd provisionazd deploy,并写明托管部署需要 Foundry Project Manager 角色。Windows 上同样需要一个可用的 Docker 环境,仓库没有分平台说明。

明确不比的几件事

  • 性能、耗时、成本:我们没有跑过任何一段代码,不谈。
  • .NET:第 14 课的 code-samples/ 目录下只有 Python 文件,托管示例也只有 .py 一份。其它课有 NN-dotnet-agent-framework.cs 这样的 .NET 实现,但本课没有,所以 .NET 侧不比。
  • 中间件:README 只在原生一侧写了 FunctionInvocationContext 的函数中间件与 ChatContext 的聊天中间件(14-middleware.ipynb 里通过 middleware=[priority_check_middleware] 注入,中间件里读改 context.result)。托管一侧我们在这门课里没有找到对应说明,不比。

从你的处境倒推

  • 图已经用 LangGraph 写好了,你要的只是一个受管的 HTTP 端点:走托管示例那条,需要改的集中在 build_chat_model()main() 两处,图本身不动。
  • 你要的是多个 agent 之间的边与事件——条件路由、fan-in 汇聚、按事件观测每个 executor 的进出:这些名字在原生 API 里是现成的,托管那条路这门课没给对应物。
  • 流程要停下来等人:两边都做得到,先想清楚接住暂停的那段代码写在哪一层,再选。
  • 你写 .NET:本课的托管示例没有 .NET 版本,得回到其它课的 dotnet-agent-framework 文件里找原生写法。

最后提醒一句:上面每个类名、参数名、环境变量名都能在仓库里搜到,但这门课持续更新,路径与接口写法都会变,动手前请以仓库最新内容为准。

以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。


本文依据 github.com/microsoft/ai-agents-for-beginners 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码, 因此不涉及运行结果、耗时与 Agent 实际表现的任何描述。 该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。 文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。

本文对照的是同一项目内的两种用法,依据均为上述仓库内容,不对两种用法做优劣排名, 选型结论只在仓库文档写明的能力边界内成立。

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。