• [对比学习LangChain和MAF-02]基本编程模式的差异(下篇)


    “上篇”我们介绍了LangChain和MAF的基本编程模式,包括如何创建Agent、如何注册工具、以及阻塞式调用流式响应的编程方式。交给Agent的任务基本上不会是一蹴而就的,需要在同一个上下文中进行多轮的推理和决策,这就需要Agent能够在多个请求之间保持会话状态,以便能够在不同的请求中共享上下文信息,从而实现更复杂的交互和推理。如果单纯的ReAct式的推理方式无法满足需求,我们还可以通过自定义推理流程来实现更复杂的逻辑控制。接下我们就来看看会话保持流程编排在LangChain和MAF中的实现方式。

    1. 会话保持

    当目前位置,我们演示的Agent都是在一个单一的请求中执行的,也就是说我们在调用Agent的RunAsync方法时传入了一个输入消息,Agent会根据这个输入消息来执行推理,并将最终的输出结果返回给我们。但是在实际应用中,Agent往往需要在多个请求之间保持会话状态,以便能够在不同的请求中共享上下文信息,从而实现更复杂的交互和推理。LangChain通过Thread来实现会话保持,而MAF则通过Session来实现会话保持。我们将在下一章中介绍MAF中的Session机制以及它与LangChain中的Thread机制之间的对比。

    1.1 LangChain

    如下的示例代码演示了如何在LangChain中实现会话保持。我们首先创建了一个Agent,并在第一次调用ainvoke方法时传入一个输入消息来执行推理。然后我们再次调用ainvoke方法,并传入一个新的输入消息来继续执行推理。由于我们在第一次调用ainvoke方法时指定了一个Thread ID(thread-1),所以第二次调用ainvoke方法时只需要指定同样的Thread ID(thread-1)就可以了,这样Agent就会将两次调用关联到同一个Thread中,从而实现会话保持。由于LangChain采用基于Checkpoint的状态管理机制,所以我们还需要为Agent指定一个Checkpointer来保存和加载Thread的状态。在这个示例中,我们使用了MemorySaver作为Checkpointer,它会将Thread的状态保存在内存中。

    from langchain.agents import create_agent
    from langchain_openai import ChatOpenAI
    from langchain.tools import tool
    from langchain_core.runnables import RunnableConfig
    from langgraph.checkpoint.memory import MemorySaver
    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],
            checkpointer=MemorySaver(),
        )
        config:RunnableConfig ={
            "configurable":{
                "thread_id":"thread-1",
            }
        }
        result = await agent.ainvoke(
            input= {"messages":[{"role":"user","content":"根据当前苏州的天气给我一些穿衣建议。"}]}, 
            config=config)
        print(result["messages"][-1].content) 
     
        print(f"\n{'-' * 50}\n")    
        result = await agent.ainvoke(
            input= {"messages":[{"role":"user","content":"偏商务一点!"}]}, 
            config=config)
        print(result["messages"][-1].content) 
        
    asyncio.run(main())
    

    对于第二次调用ainvoke方法来说,我们指定的问题是偏商务一点!,这个问题本身是没有上下文信息的,Agent无法根据这个问题来生成有意义的回答。但是由于我们在第一次调用ainvoke方法时指定了一个Thread ID(thread-1),所以第二次调用ainvoke方法时只需要指定同样的Thread ID(thread-1)就可以了,这样Agent就会将两次调用关联到同一个Thread中,从而实现会话保持。Agent在执行第二次调用时会将第一次调用的输入消息和输出结果作为上下文信息来推理,所以它能够根据第一次调用的结果来理解第二次调用的问题,并生成一个有意义的回答。

    第一轮输出:

    根据当前苏州的天气情况(**晴天,最高气温约 25°C**),给你一些穿衣建议:
    
    ### 🌤 白天穿搭
    - **上衣**:短袖T恤、薄衬衫、POLO衫都很合适  
    - **下装**:牛仔裤、休闲裤、薄款长裤或半身裙  
    - **鞋子**:运动鞋、休闲鞋、帆布鞋都很舒适
    
    ### 🌬 早晚/室内
    - 早晚可能稍微偏凉,或室内空调较足  
    - 可随身带一件 **薄外套 / 针织开衫 / 防晒衣**
    
    ### ☀️ 其他建议
    - 晴天紫外线可能偏强,**可戴帽子、太阳镜**  
    - 户外活动多的话,**记得防晒**
    
    如果你是要**通勤、出游、运动**或有特殊场合,我也可以帮你搭配得更具体一些。
    
    ---
    
    好的,偏**商务风**的话,可以这样穿,既正式又不会太热:
    
    ### 👔 男士商务休闲(25°C 适合)
    - **上衣**  - 浅色衬衫(白色 / 浅蓝 / 浅灰),**长袖可卷袖**  
      - 或薄款商务POLO衫  
    - **下装**  - 修身西裤 / 商务休闲裤(面料轻薄、透气)  
    - **外搭**(可选):  
      - 薄款西装外套(不必全程穿,空调房用)  
    - **鞋子**  - 皮鞋(德比 / 乐福鞋),或干净的商务休闲皮鞋  
    - **配件**  - 皮带同色系,腕表简约即可
    
    ### 👗 女士商务休闲
    - **上衣**  - 雪纺或真丝感衬衫(浅色系更清爽)  
    - **下装**  - 西装裤 / 直筒裤 / 及膝半身裙  
    - **外搭**  - 薄款西装 / 针织小外套  
    - **鞋子**  - 低跟鞋 / 乐福鞋 / 尖头平底鞋  
    - **配色**  - 米白、浅灰、浅蓝 + 深色下装,显得专业又不闷热
    
    ### ✅ 小提醒
    - 避免厚西装、深色全套,容易闷  
    - 面料优先选 **棉、羊毛混纺、天丝、亚麻混纺**  
    - 苏州湿度偏高,注意透气和抗皱
    
    如果你告诉我**男/女、是否见客户、是否需要穿西装**,我可以直接给你一套“今天就能照穿”的完整搭配。
    

    第二轮输出:

    好的,偏**商务一点**的话,在苏州目前**晴天、最高 25°C**的情况下,可以这样穿,既专业又不闷热:
    
    ### 👔 男士商务/商务休闲
    - **上装**  - 薄款长袖衬衫(白色、浅蓝、浅灰)  
      - 如果更正式,可选**免烫棉或轻薄西装衬衫**
    - **外搭**  - **薄西装 / 商务休闲西装**(无内衬或半衬更舒适)  
      - 不穿外套也可,但建议随身备一件应对空调
    - **下装**  - 轻薄西裤、商务休闲裤
    - **鞋子**  - 透气皮鞋、乐福鞋(Loafer)
    
    ### 👗 女士商务/商务休闲
    - **上装**  - 雪纺或真丝衬衫  
      - 薄针织上衣 + 衬衫领设计
    - **外搭**  - **薄西装外套 / 短款西装**(浅色系不显闷)
    - **下装**  - 西装裤、直筒裤  
      - 过膝或及膝的通勤裙
    - **鞋子**  - 低跟单鞋、穆勒鞋、乐福鞋
    
    ### 🎨 颜色与面料建议
    - **颜色**:白、米色、浅灰、藏蓝、雾蓝,清爽又专业  
    - **面料**:棉+涤纶混纺、羊毛混纺、天丝,**透气不易皱**
    - **避免**:厚西装、深色全套、化纤不透气面料
    
    ### ✅ 小加分项
    - 合身剪裁比厚重更显商务感  
    - 夏天商务更推荐**“轻西装 + 衬衫”**而非全套正装
    
    如果你告诉我**男/女、是否需要见客户、偏正式还是偏休闲**,我可以直接帮你搭一整套。
    

    1.2 MAF

    与LangChain在RunnableConfig中指定Thread ID来实现会话保持不同,MAF在创建Agent时就提供了一个CreateSessionAsync方法来创建一个Session对象,这个Session对象就代表了一个会话,我们可以在调用Agent的RunAsync方法时将这个Session对象作为参数传入,这样Agent就会将这个Session对象与当前的推理过程关联起来,从而实现会话保持。

    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 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 session = await agent.CreateSessionAsync();
    
    var result = await agent.RunAsync("根据当前苏州的天气给我一些穿衣建议。", session);
    Console.WriteLine(result);
    
    Console.WriteLine(new string('-', 50));
    result = await agent.RunAsync("偏商务一点!", session);
    Console.WriteLine(result);
    
    [Description("获取指定位置的天气信息")]
    static string GetWeather([Description("天气查询所在的位置")] string location)
        => $" {location}当前添加晴天,气温25°C。";
    

    我们在第一次调用RunAsync方法时创建了一个Session对象,并将其传入到RunAsync方法中来执行推理。然后我们再次调用RunAsync方法,并将同一个Session对象传入到RunAsync方法中来继续执行推理。由于我们传入的是同一个Session对象,所以两次调用的推理过程就被关联到了同一个会话中,从而实现了会话保持。Agent在执行第二次调用时会将第一次调用的输入消息和输出结果作为上下文信息来推理,所以它能够根据第一次调用的结果来理解第二次调用的问题,并生成一个有意义的回答。

    第一轮输出:

    根据**当前苏州的天气情况(晴天,气温约 25°C)**,给你一些穿衣建议:
    
    ### 👕 日常穿搭建议
    - **上衣**:短袖T恤、薄衬衫、POLO衫都很合适
    - **下装**:牛仔裤、休闲裤、薄款长裤或半身裙
    - **鞋子**:运动鞋、休闲鞋、透气的帆布鞋即可
    
    ### 🧥 额外小建议
    - 早晚可能会稍微有点凉,可以**备一件薄外套或防晒衣**
    - 晴天紫外线较强,**外出可戴帽子或墨镜**,注意防晒
    - 如果需要长时间户外活动,选择**透气、吸汗的面料**会更舒适
    
    如果你是通勤、出游还是运动场景,我也可以帮你搭配得更具体一些 😊
    

    第二轮输出:

    好的,偏**商务/商务休闲**风格,在苏州 **25°C 晴天** 的情况下,可以这样穿:
    
    ### 👔 男士商务建议
    - **上衣**  - 薄款长袖衬衫(白色、浅蓝、浅灰)
      - 如果环境不太正式,可选择高质感短袖衬衫
    - **外套**  - 薄款西装外套(可随时脱下)
    - **下装**  - 轻薄西裤或商务休闲裤
    - **鞋履**  - 透气的皮鞋、德比鞋或乐福鞋
    - **细节**  - 可不打领带,整体更清爽干练
      - 选择透气面料(如羊毛混纺、棉+弹力)
    
    ### 👠 女士商务建议
    - **上衣**  - 雪纺/真丝衬衫、简约设计的短袖西装上衣
    - **外套**  - 薄款西装外套(空调房很实用)
    - **下装**  - 西装裤、直筒裤或及膝一步裙
    - **鞋履**  - 中低跟单鞋、乐福鞋
    - **配色**  - 米白、浅灰、藏蓝、雾蓝等,既商务又不闷热
    
    ### ☀️ 天气对应小提醒
    - 25°C 偏暖,**避免厚重面料**
    - 户外走动多,优先选择**透气、防皱**的商务单品
    - 苏州湿度偏高,尽量避免全涤、闷热材质
    
    如果你是**非常正式的商务会议 / 日常办公室 / 客户拜访**,告诉我场合,我可以再帮你精确到“要不要西装外套”的程度。
    

    对于LangChain来说,我们只需要指定thread_id, Agent会利用它加载存储的Checkpoint来恢复状态。但是对于MAF来说,Session对象本身就是一个状态容器,但是这个对象的生命周期一般是短暂的,而且基于内存的状态无法解决分布式状态一致性的问题,所以依然需要一个中心化的状态存储。但是上面的这个例子是唯有问题的,因为AIProjectClient默认使用的是有状态的Response接口,服务端会自动维护Session状态。如果采用无状态的Completion接口,就需要在创建按Agent的时候指定一个ChatHistoryProvider

    下面演示程序的Agent是通过调用OpenAIClientGetChatClient方法来创建的,这个方法默认采用Completion接口来调用Chat模型,所以它是无状态的,我们需要在创建Agent的时候指定一个InMemoryChatHistoryProvider来维护会话状态。由于InMemoryChatHistoryProvider只是在进程内利用内存保存状态,所以适合单机或开发环境使用。

    var model = Environment.GetEnvironmentVariable("MODEL")!;
    var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
    var openAIUrl = Environment.GetEnvironmentVariable("OPENAI_URL")!;
    
    var openAIClient = new OpenAIClient(
        credential: new ApiKeyCredential(key: apiKey),
        options: new OpenAIClientOptions
        {
            Endpoint = new Uri(openAIUrl)
        });
    
    var agent = openAIClient
        .GetChatClient(model: model)
        .AsAIAgent(options: new ChatClientAgentOptions { 
            ChatHistoryProvider = new InMemoryChatHistoryProvider(),
            ChatOptions = new ChatOptions
            {
                Tools = [AIFunctionFactory.Create(GetWeather)]
            }
    });
    

    2. 流程编排

    对于一些复杂的任务,我们不能完全依赖LLM我们规划,更理想的方式我们自行设计整个推理流程。对于LangChain,我们可以利用LangGraph构建一个具有任意结构的状态图来定义推理流程;对于MAF,我们可以利用Workflow来构建一个具有任意结构的流程图来定义推理流程。无论是LangGraph还是Workflow,它们本质上都是一样的。

    2.1 LangChain

    对于演示的根据天气提供穿衣建议的这个例子来说,我们可以自行编排整个推理流程:

    • 用户输入城市名称;
    • 根据城市名称调用工具获取天气信息;
    • 根据天气信息调用LLM来生成穿衣建议;

    这个流程可以通过下面的代码来实现。我们首先为作为状态图的StateGraph定义了一个状态类State,这个状态类包含了城市名称、天气信息和穿衣建议三个属性。然后我们定义了两个函数get_weathergive_clothing_advice,分别用于获取天气信息和生成穿衣建议。接着我们创建了一个StateGraph,并将这两个函数作为节点添加到图中,然后我们设置get_weather作为入口节点,give_clothing_advice作为出口节点,并添加一条从get_weathergive_clothing_advice的边来定义它们之间的执行顺序。最后我们调用ainvoke方法来执行这个状态图,并传入一个包含城市名称的State对象作为输入。

    from langgraph.graph import StateGraph
    from langchain_openai import ChatOpenAI
    from dataclasses import dataclass
    from langchain.messages import AIMessage
    from dotenv import load_dotenv
    
    import asyncio
    
    load_dotenv()
    
    @dataclass
    class State:
        city: str
        weather:str|None = None
        clothing_advice:str|None = None
    
    model = ChatOpenAI(model="gpt-5.2-chat")
    
    async def get_weather(state: State) -> dict:
        """根据给定的位置获取当前天气。"""
        return {"weather": f"所在地 {state.city} 的天气是晴天,最高气温25°C。"}
    
    async def give_clothing_advice(state: State)-> dict:
        prompt = f"""所在地 {state.city} 的天气是 {state.weather}。请给我一些穿衣建议。"""
        response = await model.ainvoke(input = prompt)
        return {"clothing_advice": response.content} if isinstance(response, AIMessage) else {"clothing_advice": str(response)}
    
    async def main():
        agent = (
            StateGraph(State)
            .add_node(get_weather)
            .add_node(give_clothing_advice)
            .set_entry_point("get_weather")
            .set_finish_point("give_clothing_advice")
            .add_edge("get_weather", "give_clothing_advice")
            .compile()
           )
        result = await agent.ainvoke(input=State(city="Suzhou"))
        print(result["clothing_advice"])
        
    asyncio.run(main())
    

    输出:

    苏州晴天、最高气温 25°C,整体会比较舒适,给你一些穿衣建议:
    
    - **上装**:短袖 T 恤、薄衬衫或POLO衫都很合适;如果早晚出门,可以加一件**薄外套或针织开衫**- **下装**:长裤(牛仔裤、休闲裤)或轻薄的九分裤;怕热的话也可以选择透气的薄款长裤。
    - **鞋子**:运动鞋、休闲鞋都很舒适,透气性好的更佳。
    - **防晒**:晴天紫外线可能较强,建议带**太阳镜、遮阳帽**,也可以涂点防晒霜。
    - **其他**:苏州春季湿度有时偏高,选择**透气、吸汗**的面料会更舒服。
    
    如果你是通勤、出游或运动场景,我也可以帮你更具体地搭配。
    

    2.2 MAF

    与LangChain类似,我们也可以通过Workflow来定义一个具有任意结构的流程图来实现同样的功能。我们首先定义了两个函数GetWeatherGiveClothingAdviceAsync,分别用于获取天气信息和生成穿衣建议。然后我们创建了一个Workflow,并将这两个函数作为Executor添加到Workflow中,然后我们添加一条从GetWeatherGiveClothingAdviceAsync的边来定义它们之间的执行顺序。最后我们调用RunAsync方法来执行这个Workflow,并传入一个包含城市名称的字符串作为输入。

    using dotenv.net;
    using Microsoft.Agents.AI.Workflows;
    using OpenAI;
    using System.ClientModel;
    using System.ComponentModel;
    using System.Text;
    
    DotEnv.Load();
    
    var model = Environment.GetEnvironmentVariable("MODEL")!;
    var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
    var openAIUrl = Environment.GetEnvironmentVariable("OPENAI_URL")!;
    var openAIClient = new OpenAIClient(
        credential: new ApiKeyCredential(key: apiKey),
        options: new OpenAIClientOptions
        {
            Endpoint = new Uri(openAIUrl)
        });
    
    var weatherAccessor = new Func<string,string>(GetWeather).BindAsExecutor("get-wheather");
    var clothingAdvisor = new Func<string,ValueTask<string>>(GiveClothingAdviceAsync).BindAsExecutor("give-clothing-advice");
    var workflow = new WorkflowBuilder(weatherAccessor)
        .AddEdge(weatherAccessor, clothingAdvisor)
        .Build();
    
    await using var run = await InProcessExecution.RunAsync(workflow, "苏州");
    foreach (WorkflowEvent @event in run.NewEvents)
    {
        if (@event is ExecutorCompletedEvent completedEvent)
        {     
            Console.WriteLine($"{completedEvent.ExecutorId}: \n{completedEvent.Data}\n");
        }
    }
    
    
    static string GetWeather([Description("天气查询所在的位置")] string location) => $"{location}当前添加晴天,气温25°C。";
    async ValueTask<string> GiveClothingAdviceAsync(string weather)
    {
        var builder = new StringBuilder();
        var prompt = $"根据以下天气信息给我一些穿衣建议:{weather}";
        var result = await openAIClient.GetChatClient(model: model).CompleteChatAsync(prompt);
        foreach (var part in result.Value.Content)
        {
            builder.Append(part.Text);
        }
        return builder.ToString();
    }
    

    输出:

    get-wheather:
    苏州当前添加晴天,气温25°C。
    
    give-clothing-advice:
    根据你提供的天气信息(苏州,晴天,气温约 25°C),这是一个比较舒适、偏暖的天气,适合穿着轻便一些:
    
    **穿衣建议:**
    - **上装**:短袖T恤、薄衬衫、POLO衫都很合适;怕晒的话可以选择薄款长袖。
    - **下装**:牛仔裤、休闲裤、薄款长裤,或者裙子、短裤都可以。
    - **外套**:一般不需要外套,如果早晚稍凉或室内空调较冷,可以带一件**薄开衫或防晒衣**- **鞋子**:运动鞋、休闲鞋、帆布鞋都很舒适;出行多的话避免太闷的鞋。
    - **其他建议**  - 晴天紫外线较强,**帽子、太阳镜、防晒霜**会很实用。
      - 若长时间户外活动,选择**透气、吸汗的面料**会更舒服。
    
    如果你需要通勤、约会、运动或出游场景的具体穿搭,我也可以帮你更细化推荐。
    
  • 相关阅读:
    华为ensp单臂路由及OSPF实验
    如何提高一个网站的排名?
    【iOS】—— 六大原则和工厂模式
    【计算机网络—TCP相关】| 三次握手+四次挥手(面试不慌)
    黄河.黄土.黄种人杂志黄河.黄土.黄种人杂志社黄河.黄土.黄种人编辑部2022年第15期目录
    《C++ Primer》第2章 变量(二)
    [笔记]SSH 端口转发
    筛选图片,写JSON文件和复制
    SharpCrafters PostSharp Crack
    人工神经网络算法实战教程
  • 原文地址:https://www.cnblogs.com/artech/p/20100914/lc-maf-02