• [MAF的Agent管道详解-03]连接LLM的IChatClient对象


    IChatClient管道的最末端是一个与LLM进行交互的IChatClient对象,这个对象负责将最终的请求发送给LLM并返回响应结果。这个IChatClient对象的具体类型取决于我们使用的是什么模型以及模型的部署方式。系统提供了很多这样的IChatClient实现来支持不同的模型和部署方式。对于目前主流的LLM,我们都可以直接利用其客户端来创建一个对应的IChatClient对象.

    1. 为三种OpenAI客户端创建IChatClient对象

    OpenAIClientAzureOpenAIClient是一个与OpenAI的API进行交互的客户端,我们可以指定模型名称调用其GetChatClient方法来获取一个对应的ChatClient对象。虽然名字雷同,但是这个ChatClient类型可没有实现IChatClient接口,我们需要调用为它定义的扩展方法AsIChatClient来将它转换成一个实现了IChatClient接口的对象。

    public class AzureOpenAIClient
    {
        public override ChatClient GetChatClient(string deploymentName);
        public override ResponsesClient GetResponsesClient();
    }
    
    public class OpenAIClient
    {
        public virtual ChatClient GetChatClient(string model);
        public virtual ResponsesClient GetResponsesClient();
    }
    
    public static class OpenAIClientExtensions
    {
        public static IChatClient AsIChatClient(this ChatClient chatClient);
        public static IChatClient AsIChatClient(this ResponsesClient responseClient, string? defaultModelId = null);
    }
    

    前面说过,GetChatClient返回的ChatClient对象采用基于文本补全的无状态的Completion API来与模型进行交互,如果需要采用有状态的Responses API,需要调用GetResponsesClient方法来获取一个ResponsesClient对象。系统依然为ResponsesClient对象定义了一个AsIChatClient的扩展方法来将它转换成一个实现了IChatClient接口的对象。

    如果使用的是基于Microsoft Foundry的AIProjectClient客户端。由于它的基类是ClientConnectionProviderExtensions,我们可以调用其扩展方法GetProjectOpenAIClient得到一个ProjectOpenAIClient对象。由于ProjectOpenAIClient继承自OpenAIClient,我们同样可以调用为它定义的AsIChatClient扩展方法来将它转换成一个实现了IChatClient接口的对象。

    public class AIProjectClient : ClientConnectionProvider
    
    public static class ClientConnectionProviderExtensions
    {
        public static ProjectOpenAIClient GetProjectOpenAIClient(
            this ClientConnectionProvider connectionProvider, 
            ProjectOpenAIClientOptions options = null);
    }
    
    public class ProjectOpenAIClient : OpenAIClient
    

    2. 模拟Agent的ReAct循环

    接下来我们看看一个利用OpenAIClient创建的IChatClient对象在调用LLM的时候,提供的请求和响应内容是什么样子的。下面的代码模拟了一个Agent内部的执行流程(ReAct循环),我们使用这个Agent来根据苏州的天气给出一些着装建议。我们根据OpenAIClient创建了对应的IChatClient对象,整个流程涉及两次针对它的调用。两次调用使用同一个ChatOptions对象,我们为这个ChatOptions设置了系统指令(你是一个深谙养身之道的时尚顾问)并注册了一个用于查询天气的工具GetWeather

    using dotenv.net;
    using Microsoft.Extensions.AI;
    using OpenAI;
    using System.ClientModel;
    using System.ComponentModel;
    
    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 chatClient = openAIClient.GetResponsesClient().AsIChatClient(defaultModelId:model);
    var options = new ChatOptions
    {   
        Instructions = "你是一个深谙养身之道的时尚顾问。",
        Tools = [AIFunctionFactory.Create(GetWeather)]
    };
    var message = new ChatMessage(role: ChatRole.User, content: "根据苏州的天气给我一些着装建议。");
    List messages = [message];
    
    // First turn: user -> assistant (with function call)
    var response = await chatClient.GetResponseAsync(
        messages: messages,
        options: options);
    messages.AddRange(response.Messages);
    
    var functionCall = response.Messages.Last().Contents.OfType().Single();
    var tool = options.Tools.Single(t => t.Name == functionCall.Name);
    var toolResult = await ((AIFunction)tool).InvokeAsync(new AIFunctionArguments(functionCall.Arguments));
    var toolResultMessage = new ChatMessage(ChatRole.Tool, [new FunctionResultContent(functionCall.CallId, toolResult)]);
    messages.Add(toolResultMessage);
    
    // Second turn: user -> assistant (with tool result)
    response = await chatClient.GetResponseAsync(
        messages: messages,
        options: options);
    
    Console.WriteLine(response.Messages.Last().Text);
    static string GetWeather([Description("Location for weather query")] string location) => $"{location} 当前晴朗,气温为25°C。";
    

    我们指定查询(根据苏州的天气给我一些着装建议)和ChatOptions调用IChatClient对象。LLM经过推理任务需要调用工具函数GetWeather来获取苏州的天气信息,所以响应消息的内容列表会包含一个FunctionCallContent。在手工将响应消息添加到消息列表中后,我们利用FunctionCallContent从注册的工具列表中找到对应的工具。

    我们将LLM提供的输入参数从FunctionCallContent提取出来后,调用工具函数GetWeather得到对应的结果。接下来我们针对工具的返回结果创建一个角色为ToolChatMessage对象,并将它添加到消息列表中。最后我们再次调用IChatClient对象来获取LLM的最终回复。此时LLM就可以根据工具的返回结果来生成最终如下所示的答案:

    好的,我们就顺着苏州此刻**25°C、晴朗**的状态,从**养身 + 时尚**两个角度来搭配。
    
    ---
    
    ## 🌤️ 今日苏州着装总思路
    **关键词:清爽透气、遮阳不闷、早晚微调**
    
    25°C 属于非常舒适的温度,但苏州湿度通常不低,**选对面料比堆叠衣服更重要**。
    
    ---
    
    ## 👕 上装建议
    - **首选**  - 棉麻衬衫(浅色系:米白、浅灰、雾蓝)
      - 薄款针织或天丝T恤
    - **养身理由**  - 棉麻、天丝透气吸湿,减少湿热闷汗,对皮肤和气血运行更友好
    - **小技巧**  - 避免紧身、化纤材质,容易“闷火生湿”
    
    ---
    
    ## 👖 下装建议
    - **推荐**  - 九分直筒裤 / 轻薄阔腿裤
      - 膝下A字裙或真丝半裙
    - **颜色**  - 浅卡其、灰绿、烟粉色,有“降燥感”
    - **养身点**  - 不勒腹、不裹腿,有助于脾胃与下肢血液循环
    
    ---
    
    ## 👟 鞋履选择
    - **白色/浅色透气运动鞋**
    - **软底乐福鞋 / 平底凉鞋(包后跟更养脚)**
    - 避免全天穿完全平底或过硬的鞋,对足底经络不友好
    
    ---
    
    ## 🧥 随身加一件(很关键)
    - **薄开衫 / 防晒衬衫**
      - 室内空调 + 早晚微风时护住肩颈
      - 肩颈保暖 = 少落枕、少疲劳
    
    ---
    
    ## 🕶️ 配饰与养身小细节
    - **帽子或遮阳伞**:防晒就是防“耗气”
    - **天然材质包袋**:帆布、草编,更符合当下季节气场
    - **配色不宜过于浓烈**:春夏交替,宜“柔不宜躁”
    
    ---
    
    如果你愿意告诉我:
    -**上班 / 休闲 / 约会 / 出游**
    - 或偏**中性、优雅、运动风**
    
    我可以直接帮你搭一整套「今天就能穿出门」的苏州限定穿搭 🌿
    

    这是第一轮调用LLM提供的请求和得到的响应内容:

    {
      "model": "gpt-5.2-chat",
      "tools": [
        {
          "type": "function",
          "name": "_Main_g_GetWeather_0_1",
          "description": "",
          "parameters": {
            "type": "object",
            "required": [
              "location"
            ],
            "properties": {
              "location": {
                "description": "Location for weather query",
                "type": "string"
              }
            },
            "additionalProperties": false
          },
          "strict": null
        }
      ],
      "input": [
        {
          "type": "message",
          "role": "user",
          "content": [
            {
              "type": "input_text",
              "text": "根据苏州的天气给我一些着装建议。"
            }
          ]
        }
      ],
      "instructions": "你是一个深谙养身之道的时尚顾问。"
    }
    
    {
      "id": "resp_08fd9fcf3071918b006a000a00f53081938f105b04d924cb63",
      "object": "response",
      "created_at": 1778387456,
      "status": "completed",
      "background": false,
      "completed_at": 1778387457,
      "content_filters": [
        {
          "blocked": false,
          "source_type": "prompt",
          "content_filter_raw": [],
          "content_filter_results": {
            "hate": {
              "filtered": false,
              "severity": "safe"
            },
            "sexual": {
              "filtered": false,
              "severity": "safe"
            },
            "violence": {
              "filtered": false,
              "severity": "safe"
            },
            "self_harm": {
              "filtered": false,
              "severity": "safe"
            }
          },
          "content_filter_offsets": {
            "start_offset": 0,
            "end_offset": 49,
            "check_offset": 0
          }
        },
        {
          "blocked": false,
          "source_type": "completion",
          "content_filter_raw": [],
          "content_filter_results": {
            "hate": {
              "filtered": false,
              "severity": "safe"
            },
            "sexual": {
              "filtered": false,
              "severity": "safe"
            },
            "violence": {
              "filtered": false,
              "severity": "safe"
            },
            "self_harm": {
              "filtered": false,
              "severity": "safe"
            }
          },
          "content_filter_offsets": {
            "start_offset": 0,
            "end_offset": 1170,
            "check_offset": 0
          }
        }
      ],
      "error": null,
      "frequency_penalty": 0.0,
      "incomplete_details": null,
      "instructions": "你是一个深谙养身之道的时尚顾问。",
      "max_output_tokens": null,
      "max_tool_calls": null,
      "model": "gpt-5.2-chat",
      "output": [
        {
          "id": "rs_08fd9fcf3071918b006a000a0152f88193b91826c5aa30181a",
          "type": "reasoning",
          "summary": []
        },
        {
          "id": "fc_08fd9fcf3071918b006a000a01c6548193b850d9b453ce47f8",
          "type": "function_call",
          "status": "completed",
          "arguments": "{\"location\":\"苏州\"}",
          "call_id": "call_kYGZgvSLCPipLqtmiIqfnIDT",
          "name": "_Main_g_GetWeather_0_1"
        }
      ],
      "parallel_tool_calls": true,
      "presence_penalty": 0.0,
      "previous_response_id": null,
      "prompt_cache_key": null,
      "prompt_cache_retention": null,
      "reasoning": {
        "effort": "medium",
        "summary": null
      },
      "safety_identifier": null,
      "service_tier": "default",
      "store": true,
      "temperature": 1.0,
      "text": {
        "format": {
          "type": "text"
        },
        "verbosity": "medium"
      },
      "tool_choice": "auto",
      "tools": [
        {
          "type": "function",
          "description": null,
          "name": "_Main_g_GetWeather_0_1",
          "parameters": {
            "type": "object",
            "required": [
              "location"
            ],
            "properties": {
              "location": {
                "description": "Location for weather query",
                "type": "string"
              }
            },
            "additionalProperties": false
          },
          "strict": false
        }
      ],
      "top_logprobs": 0,
      "top_p": 0.85,
      "truncation": "disabled",
      "usage": {
        "input_tokens": 83,
        "input_tokens_details": {
          "cached_tokens": 0
        },
        "output_tokens": 43,
        "output_tokens_details": {
          "reasoning_tokens": 0
        },
        "total_tokens": 126
      },
      "user": null,
      "metadata": {}
    }
    

    这是第二轮调用LLM提供的请求和得到的响应内容:

    {
      "model": "gpt-5.2-chat",
      "tools": [
        {
          "type": "function",
          "name": "_Main_g_GetWeather_0_1",
          "description": "",
          "parameters": {
            "type": "object",
            "required": [
              "location"
            ],
            "properties": {
              "location": {
                "description": "Location for weather query",
                "type": "string"
              }
            },
            "additionalProperties": false
          },
          "strict": null
        }
      ],
      "input": [
        {
          "type": "message",
          "role": "user",
          "content": [
            {
              "type": "input_text",
              "text": "根据苏州的天气给我一些着装建议。"
            }
          ]
        },
        {
          "type": "reasoning",
          "id": "rs_08fd9fcf3071918b006a000a0152f88193b91826c5aa30181a",
          "summary": []
        },
        {
          "type": "function_call",
          "id": "fc_08fd9fcf3071918b006a000a01c6548193b850d9b453ce47f8",
          "status": "completed",
          "call_id": "call_kYGZgvSLCPipLqtmiIqfnIDT",
          "name": "_Main_g_GetWeather_0_1",
          "arguments": "{\"location\":\"苏州\"}"
        },
        {
          "type": "function_call_output",
          "call_id": "call_kYGZgvSLCPipLqtmiIqfnIDT",
          "output": "\"苏州 当前晴朗,气温为25°C。\""
        }
      ],
      "instructions": "你是一个深谙养身之道的时尚顾问。"
    }
    
    {
      "id": "resp_08fd9fcf3071918b006a000a025fbc8193a1219305fbea8789",
      "object": "response",
      "created_at": 1778387458,
      "status": "completed",
      "background": false,
      "completed_at": 1778387467,
      "content_filters": [
        {
          "blocked": false,
          "source_type": "completion",
          "content_filter_raw": [],
          "content_filter_results": {
            "hate": {
              "filtered": false,
              "severity": "safe"
            },
            "sexual": {
              "filtered": false,
              "severity": "safe"
            },
            "violence": {
              "filtered": false,
              "severity": "safe"
            },
            "self_harm": {
              "filtered": false,
              "severity": "safe"
            }
          },
          "content_filter_offsets": {
            "start_offset": 0,
            "end_offset": 1912,
            "check_offset": 0
          }
        }
      ],
      "error": null,
      "frequency_penalty": 0.0,
      "incomplete_details": null,
      "instructions": "你是一个深谙养身之道的时尚顾问。",
      "max_output_tokens": null,
      "max_tool_calls": null,
      "model": "gpt-5.2-chat",
      "output": [
        {
          "id": "msg_08fd9fcf3071918b006a000a02c53c819385543898398da88e",
          "type": "message",
          "status": "completed",
          "content": [
            {
              "type": "output_text",
              "annotations": [],
              "logprobs": [],
              "text": "...(同上面展示的LLM最终回复内容)..."
            }
          ],
          "role": "assistant"
        }
      ],
      "parallel_tool_calls": true,
      "presence_penalty": 0.0,
      "previous_response_id": null,
      "prompt_cache_key": null,
      "prompt_cache_retention": null,
      "reasoning": {
        "effort": "medium",
        "summary": null
      },
      "safety_identifier": null,
      "service_tier": "default",
      "store": true,
      "temperature": 1.0,
      "text": {
        "format": {
          "type": "text"
        },
        "verbosity": "medium"
      },
      "tool_choice": "auto",
      "tools": [
        {
          "type": "function",
          "description": null,
          "name": "_Main_g_GetWeather_0_1",
          "parameters": {
            "type": "object",
            "required": [
              "location"
            ],
            "properties": {
              "location": {
                "description": "Location for weather query",
                "type": "string"
              }
            },
            "additionalProperties": false
          },
          "strict": false
        }
      ],
      "top_logprobs": 0,
      "top_p": 0.85,
      "truncation": "disabled",
      "usage": {
        "input_tokens": 157,
        "input_tokens_details": {
          "cached_tokens": 0
        },
        "output_tokens": 612,
        "output_tokens_details": {
          "reasoning_tokens": 0
        },
        "total_tokens": 769
      },
      "user": null,
      "metadata": {}
    }
    
  • 相关阅读:
    Mac电脑VSCode配置PHP开发环境
    近期 yyds 的 GitHub 项目
    自动驾驶感知的AI力量:大模型在车辆感知中的应用
    记GitLab服务器迁移后SSH访问无法生效的问题解决过程
    LeetCode_Array_1004. Max Consecutive Ones III 最大连续1的个数 III【滑动数组】【Java】【中等】
    基于工业互联网标识解析的数字化治理研究
    游戏海外运营需要准备什么?
    MySQL 中的锁类型有哪些
    使用SSH反向转发服务器上的请求到个人电脑
    共享平台如何提高财务的分账记账效率?
  • 原文地址:https://www.cnblogs.com/artech/p/20194058/agent-pipeline-03