惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog
I
InfoQ
Y
Y Combinator Blog
The Last Watchdog
The Last Watchdog
博客园_首页
The Cloudflare Blog
博客园 - 【当耐特】
Engineering at Meta
Engineering at Meta
罗磊的独立博客
月光博客
月光博客
V
V2EX
大猫的无限游戏
大猫的无限游戏
腾讯CDC
GbyAI
GbyAI
云风的 BLOG
云风的 BLOG
Stack Overflow Blog
Stack Overflow Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Attack and Defense Labs
Attack and Defense Labs
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
B
Blog RSS Feed
Webroot Blog
Webroot Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
N
Netflix TechBlog - Medium
量子位
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Vercel News
Vercel News
C
CERT Recently Published Vulnerability Notes
人人都是产品经理
人人都是产品经理
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Recent Announcements
Recent Announcements
Cyberwarzone
Cyberwarzone
G
Google Developers Blog
H
Heimdal Security Blog
MyScale Blog
MyScale Blog
The Register - Security
The Register - Security
博客园 - 三生石上(FineUI控件)
小众软件
小众软件
aimingoo的专栏
aimingoo的专栏
T
Tenable Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
O
OpenAI News
C
Check Point Blog
Forbes - Security
Forbes - Security
SecWiki News
SecWiki News
K
Kaspersky official blog
The GitHub Blog
The GitHub Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
F
Full Disclosure
阮一峰的网络日志
阮一峰的网络日志

博客园 - Artech

[MAF预定义的AIContextProvider-05]CompactionProvider——采用多种策略压缩对话历史 [MAF预定义的AIContextProvider-04]Mem0Provider——长期记忆基于的云端解决方案 [MAF预定义的AIContextProvider-03]ChatHistoryMemoryProvider——赋予Agent从经验中学习的能力 [MAF预定义的AIContextProvider-02]AgentSkillsProvider——将Agent Skills引入MAF [MAF预定义的AIContextProvider-01]TextSearchProvider——RAG在MAF中的实现 [MAF预定义ChatClient中间件-09]MessageInjectingChatClient-赋予工具消息注入的能力 [MAF预定义ChatClient中间件-08]OpenTelemetryChatClient-实现链路跟踪和性能监控 [MAF预定义ChatClient中间件-07]PerServiceCallChatHistoryPersistingChatClient——基于ReAct循环的一步一存档 [MAF预定义ChatClient中间件-06]利用ImageGeneratingChatClient开发专业图片生成Agent [MAF预定义ChatClient中间件-05]动态修改ChatOptions和请求消息 [MAF预定义ChatClient中间件-04]ReducingChatClient——精减对话历史又不丢失基本语义 [MAF预定义ChatClient中间件-03]CachingChatClient——利用缓存省钱省时间 [MAF预定义ChatClient中间件-02]FunctionInvokingChatClient——实现ReAct循环和人机交互的大功臣 [MAF预定义ChatClient中间件-01]LoggingChatClient——在调用LLM前后输出日志 [MAF的Agent管道详解-07]利用AIAgent中间件构建Agent管道 [MAF的Agent管道详解-06]ChatClientAgent对IChatClient和输入输出增强管道的整合 [MAF的Agent管道详解-06]ChatClientAgent对IChatClient和输入输出增强管道的整合 - Artech [MAF的Agent管道详解-05]对话历史的持久化和输入输出的增强 [MAF的Agent管道详解-04]如何让LLM按照要求的结构输出数据? [MAF的Agent管道详解-03]连接LLM的IChatClient对象 [MAF的Agent管道详解-02]IChatClient管道如何完美连接大模型? [MAF的Agent管道详解-01]塑智能体边界,从AIAgent抽象类开始 [对比学习LangChain和MAF-04]针对消息的设计 [对比学习LangChain和MAF-03]完全不同的Agent设计哲学 [对比学习LangChain和MAF-02]基本编程模式的差异(下篇) 我所理解的Python元模型 除了按值和引用,方法参数的第三种传递方式 方法的三种调用形式 可以调用Null的实例方法吗? 自定义Key类型的字典无法序列化的N种解决方案 为什么ASP.NET Core的路由处理器可以使用一个任意类型的Delegate 深入解析ASP.NET Core MVC的模块化设计[下篇] 深入解析ASP.NET Core MVC的模块化设计[上篇] ASP.NET Core MVC应用模型的构建[4]: Action的选择 ASP.NET Core MVC应用模型的构建[3]: Controller的收集 ASP.NET Core MVC应用模型的构建[2]: 定制应用模型
[对比学习LangChain和MAF-01]基本编程模式的差异(上篇)
Artech · 2026-05-20 · via 博客园 - Artech

作为一个资深的.NET开发者,当初决定转移到Agent赛道时,我首先选择的是微软的Semantic Kernel、AutoGen以及后来将二者统一在一起的MAF(Microsoft Agent Framework),但是我总觉得这个框架在设计上差点意思。再加上既然都决定决定投入到AI领域,就没有必要继续把自己困在.NET的茧房,于是选择了LangChain。经过对LangChain深入学习和使用后,再回头看看MAF,会有不一样的体会。通过对比,我觉得我现在对MAF有了更深的理解,而且我发现通过对比学习能够有效了理解MAF的精髓所在。

在这个系列中,我们将通过与LangChain对比的方式来介绍MAF的编程模式和架构设计。我们知道LangChain的主流开发语言是Python,如果通过Python来介绍MAF,这样比较起来更加直观。但是如果选择MAF,C#明显是更好的选择,所以我还是决定使用C#来介绍MAF。好在这两种语言都很“人性化”,即使你不会用,总归能看得懂。

MAF提供了多样化的Agent,我们这里主要关注采用Chat模型,采用多轮对话进行推理的ChatClientAgent。LangChain提供了多种编程模式来创建Agent,最常见的就是使用create_agent函数根据指定的模型和工具来创建Agent。MAF也是如此,我们不仅可用调用ChatClientAgent的构造函数利用提供的LLM客户端和工具来创建Agent,它还通过定义扩展方法的形式直接将LLM客户端转换成ChatClientAgent。对应一些复杂的推理任务,LangChain的解决方式是利用LangGraph构建具有对应结构的状态图来规范推理流程,而MAF对应的解决方案是采用工作流,状态图和工作流其实就是同一事物的两种不同说法而已。多轮对话需要通过一个上下文来维护状态,LangChain将此上下文称为Thread,而MAF则称之为Session。

1. 从最简单的Agent开始

最简单的Agent只需要一个LLM客户端就可以了。LLM客户端在LangChain中通过BaseLanguageModel来表示,但是它主要代表基于文本补全的Completion模型,继承于它的BaseChatModel才真正代表了Chat模型。具体的LLM客户端依赖于LLM类型和对应的服务提供商。下面的演示程序使用部署在Azure上的GPT-Chat模型(gpt-5.2-chat)。

1.1 LangChain

如下所示的是使用LangChain创建Agent的示例代码。如代码片段所示,我们针对上述的模型名称(gpt-5.2-chat)创建了一个ChatOpenAI对象,并将其作为参数传递给create_agent函数来创建Agent。然后调用异步方法ainvoke来执行Agent。

from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
import asyncio

load_dotenv()

async def main():
    agent = create_agent(model=ChatOpenAI(model="gpt-5.2-chat"))
    result = await agent.ainvoke(input= {
        "messages":[{"role":"user","content":"目前的天气是晴天,气温25°C。请给我一些穿衣建议。"}]
    })
    print(result["messages"][-1].content)
    
asyncio.run(main())

LangChain中的LLM客户端组件都会利用环境变量来定义配置,并且它会自动读取环境变量,这一点与MAF的LLM客户端设计不太一样,后者一般要求自行读取并设置环境变量并显式传递给LLM客户端。ChatOpenAI所需的Endpoint地址和API-Key就定义在如下所示的.env文件中,并利用python-dotenv库的load_dotenv函数来加载环境变量。

OPENAI_BASE_URL=https://eap2410.cognitiveservices.azure.com/openai/v1
OPENAI_API_KEY=**********************

由于采用交谈式的推理方式,作为输入的字典需要利用messages键来指定对话消息列表,每条消息包含角色(role)和内容(content)。在这个示例中,我们只提供了一条用户消息,Agent会根据这条消息来生成穿衣建议。输出同样具有类似的结构,我们通过访问输出字典中的messages键来获取生成的消息列表,并打印最后一条消息的内容。

晴天、25°C 的天气属于**比较舒适偏暖**的状态,穿搭可以以**清爽、透气、防晒**为主。以下是一些具体建议:

### 👕 上装
- 短袖T恤、POLO衫  
- 薄款衬衫(棉或亚麻材质更透气)
- 如果在空调环境久待,可备一件**薄外套或开衫**

### 👖 下装
- 牛仔裤、休闲长裤  
- 九分裤、轻薄休闲裤  
- 喜欢清凉一点的,可以选择**及膝短裤或半身裙**

### 👟 鞋子
- 运动鞋、帆布鞋  
- 乐福鞋、平底鞋  
- 天气干燥时也可以穿凉鞋(通勤场合注意正式度)

### 🧢 配件建议
- 遮阳帽、太阳镜(晴天防晒)
- 防晒霜(尤其是长时间户外活动)
- 轻便背包或手提包

### ✅ 穿搭小提醒
- 选择**浅色系**衣服更不吸热  
- 面料尽量选**棉、麻、莫代尔**等透气材质  
- 早晚如果有风,体感可能稍凉,避免穿得太单薄

如果你要去的是**上班 / 约会 / 运动 / 旅行**,我也可以帮你给出更具体的搭配方案。

OpenAI的API具有两种形式:

  • Completion:属于早期的补全式生成接口,采用无状态设计。开发者需要手动构建包含systemuserassistant角色的消息,并将完整的历史记录发送给模型以维持对话连贯性;
  • Response:旨在统一所有生成能力(文本、音频、视觉等)的下一代接口。它具有有状态(Stateful)管理能力,允许通过传递 previous_response_id 自动关联上下文,无需每次都发送完整的历史消息;

ChatOpenAI默认情况下会使用Completion接口来调用Chat模型,但是可以通过use_responses_api参数来切换到Response接口。

agent = create_agent(model=ChatOpenAI(model="gpt-5.2-chat",use_responses_api=True))

1.2 MAF

上面这个简单的例子在MAF中可以玩出很多花样。首先,针对部署在Azure上的GPT-Chat模型,我们可以采用如下三种不同的客户端:

  • OpenAIClient:微软官方提供的OpenAIClient类,支持Azure OpenAI服务的Chat模型,提供了丰富的功能和配置选项;
  • AzureOpenAIClient:社区提供的AzureOpenAIClient类,专门针对Azure OpenAI服务进行了优化,简化了配置和使用流程;
  • AIProjectClient:这是微软最新的Azure AI Foundry (原Azure AI Studio) 推出的项目级客户端。用于管理和调用整个AI项目的资源,而不仅仅是单个模型;

我们可以利用它们来创建ChatClientAgent。虽然使用同一个部署的模型,但是客户端采用的Endpoint地址却不同。由于Python使用了python-dotenv,这里使用它的.NET版本dotenv.net,所以我们依然将Endpoint地址、模型和API-Key定义在.env文件中,并利用dotenv.net库的DotEnv类来加载环境变量。

Foundry_PROJECT_URL=https://eap2410.services.ai.azure.com/api/projects/ai-learning
OPENAI_URL=https://eap2410.cognitiveservices.azure.com/openai/v1
AZURE_OPENAI_URL=https://eap2410.cognitiveservices.azure.com/
MODEL=gpt-5.2-chat
API_KEY=**********************

我们的程序会同时演示上述三种客户端的使用方式来创建Agent,并调用Agent来执行同样的推理任务。为此我们需要添加如下所示的NuGet包引用:

<PackageReference Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
<PackageReference Include="Azure.AI.Projects" Version="2.1.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="dotenv.net" Version="4.0.2" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.3.0" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.3.0" />
<PackageReference Include="Microsoft.Extensions.AI.Ollama" Version="9.7.0-preview.1.25356.2" />
<PackageReference Include="OpenAI" Version="2.10.0" />

如下所示的是完整的示例代码。我们首先加载环境变量,然后创建三个不同客户端的Agent,并调用它们来执行同样的推理任务。如代码片段所示,我们根据指定的地址和API-KEY创建了OpenAIClientAzureOpenAIClient,并指定模型名称调用其GetChatClient方法得到一个ChatClient对象,然后直接调用它的扩展方法AsAIAgent来创建ChatClientAgent。对于AIProjectClient,我们直接调用它的扩展方法AsAIAgent来创建ChatClientAgent,并在参数中指定模型名称和一些指令。

using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using dotenv.net;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Chat;
using OpenAI.Responses;
using System.ClientModel;

DotEnv.Load();

var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var openAIUrl = Environment.GetEnvironmentVariable("OPENAI_URL")!;
var azureOpenAIUrl = Environment.GetEnvironmentVariable("AZURE_OPENAI_URL")!;
var foundryProjectUrl = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_URL")!;
var prompt = "目前的天气是晴天,气温25°C。请给我一些穿衣建议。";

// 1.Create an OpenAI client using the OpenAI SDK
var openAIClient = new OpenAIClient(
    credential: new ApiKeyCredential(key: apiKey),
    options: new OpenAIClientOptions
    {
        Endpoint = new Uri(openAIUrl)
    });

var agent = openAIClient.GetChatClient(model: model).AsAIAgent();
var result = await agent.RunAsync(message: prompt);
Console.WriteLine(result);

// 2.Create an OpenAI client using the Azure OpenAI SDK
var azureOpenAIClient = new AzureOpenAIClient(
    endpoint: new Uri(azureOpenAIUrl),
    credential: new ApiKeyCredential(key: apiKey));
agent = azureOpenAIClient.GetChatClient(deploymentName:model).AsAIAgent();


result = await agent.RunAsync(message: prompt);
Console.WriteLine(result);

// 3.Create an AI Project client using the Azure AI Projects SDK
var aiProjectClient = new AIProjectClient(
    endpoint: new Uri(foundryProjectUrl),
    tokenProvider: new DefaultAzureCredential());

agent = aiProjectClient.AsAIAgent(model: model, instructions: "You are a helpful assistant.");
result = await agent.RunAsync(message: prompt);
Console.WriteLine(result);

最后我们调用每个Agent的RunAsync方法来执行推理任务。ChatClientAgent和LangChain的Agent一样,都是基于对话的推理方式,所以它们作为交谈历史的消息列表是输入、输出以及执行过程中的状态的核心内容。只是ChatClientAgentRunAsync方法做了封装,允许我们直接指定一个字符串作为输入消息。我们从输出结果中依然会得到我们希望的内容:

在**晴天、气温约 25°C**的情况下,整体体感会比较舒适偏暖,穿搭可以以**清爽、透气**为主:

### 👕 上装建议
- **短袖T恤 / Polo衫 / 薄衬衫**
  选择棉、麻或速干面料,透气又吸汗。
- 如果需要防晒或室内空调较冷,可带一件**薄外套 / 防晒衣**。

### 👖 下装建议
- **长裤**:薄款牛仔裤、休闲裤、九分裤都很合适。
- **短裤 / 裙子**:户外活动或休闲出行非常舒服。

### 👟 鞋子建议
- **运动鞋 / 帆布鞋 / 休闲鞋**
- 如果活动轻松,也可以选择**透气的凉鞋**

### 🧢 配件建议
- **遮阳帽 / 太阳镜**:晴天防晒必备
- **防晒霜**:尤其是中午外出时
- 可携带一瓶水,防止轻微脱水

### ✅ 小提醒
- 早晚如果有微风,体感可能略凉,**薄外套会更灵活**
- 如果需要长时间户外活动,尽量选择**浅色衣物**,更凉爽

如果你要**上班、约会、运动或出游**,我也可以帮你做更具体的搭配建议 😊

OpenAIClientAzureOpenAIClientGetChatClient方法采用的是Completion接口来调用Chat模型,所以我们需要在输入消息中构建完整的对话历史来维持上下文。而AIProjectClientAsAIAgent方法默认采用Response接口来调用Chat模型,它会自动管理对话状态,我们只需要提供当前的输入消息即可,无需担心上下文维护的问题。OpenAIClientAzureOpenAIClient可以调用GetResponseClient方法切换到Response接口来调用Chat模型。

// 1.Create an OpenAI client using the OpenAI SDK
var openAIClient = new OpenAIClient(
    credential: new ApiKeyCredential(key: apiKey),
    options: new OpenAIClientOptions
    {
        Endpoint = new Uri(openAIUrl)
    });

var agent = openAIClient.GetResponsesClient().AsAIAgent(model: model);
var result = await agent.RunAsync(message: prompt);
Console.WriteLine(result);

// 2.Create an OpenAI client using the Azure OpenAI SDK
var azureOpenAIClient = new AzureOpenAIClient(
    endpoint: new Uri(azureOpenAIUrl),
    credential: new ApiKeyCredential(key: apiKey));
agent = azureOpenAIClient.GetResponsesClient().AsAIAgent(model: model);
result = await agent.RunAsync(message: prompt);
Console.WriteLine(result);

2. 注册工具

只有LLM的Agent相当于一个只有大脑的“人”,它只能通过对话来进行推理和决策,无法直接与外部环境进行交互。工具就相当于这个“人”的手脚,能够让Agent执行一些具体的操作来获取信息、处理数据或者完成任务。前面实例创建的Agent没有什么实质上的作用,为了让Agent提供着装建议,我们需要为Agent提供天气信息。这本质上是RAG的做法:我们利用查询检索并向LLM提供它并不具有的知识作为推理上下文。更理想的做法是让Agent自己去查询天气信息,这样就不需要我们事先准备好天气信息了。为此,我们可以为Agent注册一个查询天气的工具,让它在需要的时候调用这个工具来获取天气信息。

2.1 LangChain

如下的程序演示了LangChain注册工具的典型方式。我们首先定义了一个工具函数get_weather,并使用@tool装饰器将其注册为一个工具。这个工具函数接受一个位置参数,并返回该位置的天气信息。在这个示例中,我们只是返回了一个固定的天气信息,实际应用中可以通过调用天气API来获取真实的天气数据。然后我们在创建Agent时将这个工具作为参数传递给create_agent函数,这样Agent就可以在推理过程中调用这个工具来获取天气信息。

from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain.tools import tool
from dotenv import load_dotenv
import asyncio

load_dotenv()

@tool
def get_weather(location: str) -> str:
    """根据给定的位置获取当前天气。"""
    return f"所在地 {location} 的天气是晴天,最高气温25°C。"

async def main():
    agent = create_agent(
        model=ChatOpenAI(model="gpt-5.2-chat"),
        tools=[get_weather],)
    result = await agent.ainvoke(input= {
        "messages":[{"role":"user","content":"根据当前苏州的天气给我一些穿衣建议。"}]
    })
    print(result["messages"][-1].content)
    
asyncio.run(main())

输出:

好的!根据**当前苏州天气:晴天,最高气温约 25°C**,给你一些穿衣建议:

### 👕 白天穿搭
- **上衣**:短袖 T 恤、薄衬衫、POLO 衫都很合适  
- **下装**:牛仔裤、休闲裤、薄款长裤或半身裙  
- **鞋子**:运动鞋、帆布鞋、透气的休闲鞋即可  

### 🧥 早晚/室内
- 早晚可能稍微凉一点,或在空调房里:
  - 建议带一件**薄外套 / 开衫 / 轻薄风衣**

### 🌞 防晒与配件
- 晴天紫外线可能偏强:
  - **帽子、太阳镜**
  - **防晒霜**
- 出汗较多的话,选择**透气、吸汗面料**会更舒服

### ✅ 总体建议
👉 **“短袖 + 薄外套”是最稳妥的搭配**,既不热也能应对温差。

如果你是**上班通勤、出游、运动或带孩子出门**,我也可以给你更具体的搭配建议。

2.2 MAF

在MAF中注册工具的方式与LangChain类似。我们首先定义了一个工具函数GetWeather,并使用Description特性将其注册为一个工具。这个工具函数接受一个位置参数,并返回该位置的天气信息。在这个示例中,我们同样返回了一个固定的天气信息,实际应用中可以通过调用天气API来获取真实的天气数据。然后我们在创建Agent时将这个工具作为参数传递给AsAIAgent方法,这样Agent就可以在推理过程中调用这个工具来获取天气信息。

using Azure.AI.Projects;
using Azure.Identity;
using dotenv.net;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
using OpenAI.Responses;
using System.ComponentModel;

DotEnv.Load();

var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var foundryProjectUrl = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_URL")!;

var prompt = "根据当前苏州的天气给我一些穿衣建议。";

var aiProjectClient = new AIProjectClient(
    endpoint: new Uri(foundryProjectUrl),
    tokenProvider: new DefaultAzureCredential());

var agent = aiProjectClient.AsAIAgent(
    model: model, 
    instructions: "You are a helpful assistant.",
    tools: [AIFunctionFactory.Create(GetWeather)]);
var result = await agent.RunAsync(message: prompt);
Console.WriteLine(result);

[Description("获取指定位置的天气信息")]
static string GetWeather([Description("天气查询所在的位置")] string location)
    => $" {location}当前添加晴天,气温25°C。";

输出:

根据当前**苏州晴天、气温约 25°C**的天气情况,给你一些穿衣建议:

### 👕 日常出行
- **上装**:短袖T恤、薄衬衫都很合适,怕晒的话可以选长袖但要透气。
- **下装**:薄款长裤、休闲裤或半身裙都可以,体感会比较舒适。
- **外套**:白天基本不需要外套,但如果早晚出门或在空调房久待,可带一件**薄开衫/防晒衣**。

### 👟 鞋子
- 透气的运动鞋、休闲鞋或凉鞋都很合适,适合步行。

### 🌞 防晒与小提醒
- 晴天紫外线偏强,建议:
  - 戴**遮阳帽 / 太阳镜**
  - 涂**防晒霜**
- 多喝水,避免中午时段长时间暴晒。

如果你是**通勤、旅游、还是户外活动**,我也可以帮你更具体地搭配 👍

3 流式响应

Agent执行在绝大部分场景下都是一个长耗时操作,执行几分钟、几小时甚至几天都是很常见的。如果等到Agent执行完成才返回结果,那么用户体验就会非常差了。流式响应(Streaming Response)就是为了解决这个问题而设计的,它允许Agent在执行过程中逐步返回结果,让用户能够实时看到Agent的推理过程和中间结果,从而提升交互的流畅性和响应速度。

3.1 LangChain

对于上面演示的这个实例,我们只需要将针对ainvoke方法的调用改成astream方法的调用就可以了。astream方法会返回一个异步生成器,我们可以通过异步迭代的方式来获取Agent执行过程中逐步返回的结果。如代码片段所示,我们在异步迭代中获取每个返回的结果块,并提取其中的最后一个消息来打印输出。通过这种方式,我们可以在Agent执行的过程中实时看到它的推理结果,而不需要等到整个执行完成。LangChain定义了多种流模式,默认的values会在每次单步推理完成后更新状态的values,并将更新后的状态作为结果返回。

from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain.tools import tool
from dotenv import load_dotenv
import asyncio

load_dotenv()

@tool
def get_weather(location: str) -> str:
    """根据给定的位置获取当前天气。"""
    return f"所在地 {location} 的天气是晴天,最高气温25°C。"

async def main():
    agent = create_agent(
        model=ChatOpenAI(model="gpt-5.2-chat"),
        tools=[get_weather],)
    async for chunk in agent.astream(input= {
        "messages":[{"role":"user","content":"根据当前苏州的天气给我一些穿衣建议。"}]
    }):
       result = chunk["model"] if "model" in chunk else chunk["tools"]
       message = result["messages"][-1]
       message.pretty_print()   
    
asyncio.run(main())

输出:

================================== Ai Message ==================================
Tool Calls:
  get_weather (call_9TyRU0zLwY4tOZf8Hufy5qrJ)
 Call ID: call_9TyRU0zLwY4tOZf8Hufy5qrJ
  Args:
    location: 苏州
================================= Tool Message =================================
Name: get_weather

所在地 苏州 的天气是晴天,最高气温25°C。
================================== Ai Message ==================================

好的,根据**当前苏州的天气情况(晴天,最高气温约 25°C)**,给你一些穿衣建议:

### 🌤️ 白天穿搭建议
- **上衣**:  
  - 薄款长袖衬衫 / T 恤 / 薄针织衫  
  - 如果怕晒,可以选**透气的长袖**或带防晒功能的衣服
- **下装**:  
  - 牛仔裤、休闲裤、薄款西裤  
  - 也可以穿半身裙或连衣裙,体感会更清爽
- **鞋子**:  
  - 运动鞋、帆布鞋、乐福鞋  
  - 如果走路多,建议选择透气舒适的鞋

### 🌅 早晚与细节
- 早晚可能稍微偏凉或有风,**带一件薄外套/开衫**会更稳妥
- 晴天紫外线偏强:
  - ☀️ 帽子、太阳镜
  - 🧴 防晒霜(尤其是中午前后)

### 💡 体感小提醒
- 25°C 属于**不冷不热、偏舒适**的温度  
- 活动量大时容易出汗,建议选择**吸汗、透气的面料**

如果你是**上班通勤 / 出游 / 运动 / 带小孩**,我也可以按具体场景再帮你细化穿搭 👍

输出结果体现了Agent的执行流程:

  • 由于工具被注册到Agent中,工具相关的描述会作为调用LLM提示词的一部分。对于指定的问题(根据当前苏州的天气给我一些穿衣建议),LLM经过推理认为在提供最终答案之前需要先调用工具来获取天气信息,此时它会生成一个AIMessage,并将工具调用信息包含在其中,具体的工具调用信息不仅包括工具名称(get_weather),表示当前工具调用的ID(call_9TyRU0zLwY4tOZf8Hufy5qrJ),还有它根据工具的输入Schema从请求中提取的参数列表(Args);
  • Agent接收到这个AIMessage后会查看是否具有工具调用,如果没有工具调用信息,那么它就会将这个AIMessage作为最终的输出结果返回;如果有工具调用信息,那么它就会根据工具调用信息来执行对应的工具,在这个示例中就是调用get_weather工具来获取天气信息。工具调用的结果被封装在一个ToolMessage中被添加到作为对话历史的消息列表中。对于我们的例子来说,get_weather工具被调用后返回了一个ToolMessage,其中包含了工具的输出结果(所在地 苏州 的天气是晴天,最高气温25°C)。所有所有工具执行完毕(采用并发执行),Agent会将新的对话历史发送给LLM来继续推理,直到LLM不再生成新的工具调用信息为止,此时Agent会将最后一个AIMessage作为最终的输出结果返回。在这个示例中,Agent在执行完工具调用后又生成了一个新的AIMessage,这个AIMessage包含了根据工具输出的天气信息生成的穿衣建议。
  • LLM根据新的对话历史继续推理认为根据目前拥有的信息(所在地 苏州 的天气是晴天,最高气温25°C)已经足够生成穿衣建议了,所以它就没有再生成新的工具调用信息,而是直接将穿衣建议作为输出结果返回了Agent。
  • 按照前面的逻辑,Agent接收到这个AIMessage后会发现它没有工具调用信息了,于是就将这个AIMessage作为最终的输出结果返回了用户;

3.2 MAF

我们同样将MAF版本做类似的修改来支持流式响应。MAF的ChatClientAgentRunAsync方法也有一个对应的RunStreamingAsync方法来支持流式响应。这个方法会返回一个异步生成器,我们可以通过异步迭代的方式来获取Agent执行过程中逐步返回的结果。与LangChain不同的是,MAF在流式响应中直接将LLM生成的原始内容作为结果返回,而不是封装在一个特定结构的字典中,所以我们可以直接打印输出。如下所示的是修改后的示例代码:

using Azure.AI.Projects;
using Azure.Identity;
using dotenv.net;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
using OpenAI.Responses;
using System.ComponentModel;

DotEnv.Load();

var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var foundryProjectUrl = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_URL")!;

var prompt = "根据当前苏州的天气给我一些穿衣建议。";

var aiProjectClient = new AIProjectClient(
    endpoint: new Uri(foundryProjectUrl),
    tokenProvider: new DefaultAzureCredential());

var agent = aiProjectClient.AsAIAgent(
    model: model, 
    instructions: "You are a helpful assistant.",
    tools: [AIFunctionFactory.Create(GetWeather)]);

await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(message: prompt))
{
    Console.Write(update.RawRepresentation);
}

[Description("获取指定位置的天气信息")]
static string GetWeather([Description("天气查询所在的位置")] string location)
    => $" {location}当前添加晴天,气温25°C。";

但是这样修改与Langchain版本的效果不太一样了,它会实现类似于你在使用ChatGPT时逐字输出的效果,而不是等到每一步推理完成后才输出结果了。