C#调用 AI学习从0开始-第2阶段(Function Calling+工具调用智能体)-第9天实战

基于上一节基础跑通实现FunctionCalling,上一节:https://blog.csdn.net/weixin_42800530/article/details/161492135?spm=1011.2124.3001.6209

1. 代码实现 FunctionCalling功能

csharp 复制代码
using ConsoleApp1.Common;
using ConsoleApp1.Model;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace ConsoleApp1.BLL
{
    public class CommonClass
    {
        CommonHelper commHelper = new CommonHelper();

        HttpClient client ;
        string apiKey ="";  //此处写你申请的API Key
        string url = "";

        public CommonClass()
        {
            apiKey = ConfigCommon.apiKey;  //此处写你申请的API Key
            url = ConfigCommon.url_chat;

            client = new HttpClient();
            client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
        }       


        //1. 添加:保存对话历史
        private List<ChatMessage> _history=new List<ChatMessage>();

        public async Task<string> CallAPIWithTools(string userMessage, object[] tools)
        {
            string sOutValue = "";

            _history.Add(new ChatMessage("user", userMessage));

            //请求体 第1次请求:带tools参数
            var requestBody = new
            {
                model = "qwen-turbo",
                messages = _history,  //MessagesIn
                tools = tools,
                tool_choice = "auto",  //让AI自动决定是否调用工具
                stream = false
            };

            var json = JsonSerializer.Serialize(requestBody);
            var content = new StringContent(json, Encoding.UTF8, "application/json");
            var response = await client.PostAsync(url, content);  //1. 第一次调用AI接口
            var responseString = await response.Content.ReadAsStringAsync();

            if (!response.IsSuccessStatusCode)
            {
                throw new Exception($"API调用失败: {response.StatusCode}");
            }

            //2. 解析AI出参,判断是否需要调用方法(第一次出参)
            FunctionCallResponse resp1 = ParseFunctionCallResponse(responseString);
            if (resp1.FinishReason == "tool_calls")
            {
                //3. 把AI的tool_calls响应加入历史
                _history.Add(resp1.Message);

                //4. 执行方法
                var toolCall = resp1.ToolCalls[0];
                string toolResult = commHelper.ExecuteTool(toolCall.Function.Name, toolCall.Function.Arguments);

                //5. 把工具执行结果加入历史(tool角色)
                _history.Add(new ChatMessage("tool", toolResult)
                {
                    ToolCallId = toolCall.Id
                });

                //6. 第2次请求,不带tools,让AI根据结果生成回答
                var resp2 = await CallAPIWithoutTools(_history);
                _history.Add(resp2.Message);
                sOutValue = resp2.Message.Content;
            }
            else
            {
                sOutValue = resp1.Message.Content;
            }

            return sOutValue;
        }

        //解析Function Calling响应
        public FunctionCallResponse ParseFunctionCallResponse(string jsonString)
        {
            FunctionCallResponse result = new FunctionCallResponse();

            var doc = JsonDocument.Parse(jsonString);
            var root = doc.RootElement;

            //解析Finish_Reason
            result.FinishReason = root.GetProperty("choices")[0].GetProperty("finish_reason").GetString();
            //解析message
            var messageElement = root.GetProperty("choices")[0].GetProperty("message");
            result.Message = new ChatMessage(
                messageElement.GetProperty("role").GetString(),
                messageElement.GetProperty("content").GetString() ?? ""
                );

            //解析tool_calls
            if(messageElement.TryGetProperty("tool_calls",out var toolCallsElement))
            {
                result.ToolCalls = new List<ToolCall>();
                foreach(var tc in toolCallsElement.EnumerateArray())
                {
                    var toolCall = new ToolCall
                    {
                        Id = tc.GetProperty("id").GetString(),
                        Type = tc.GetProperty("type").GetString(),
                        Function = new FunctionCall
                        {
                            Name = tc.GetProperty("function").GetProperty("name").GetString(),
                            Arguments = tc.GetProperty("function").GetProperty("arguments").GetString()
                        }

                    };
                    result.ToolCalls.Add(toolCall);
                }
            }

            return result;
        }


        /// <summary>
        /// 不带tools的API调用(用于第2次请求)
        /// </summary>
        private async Task<ChatResponse> CallAPIWithoutTools(List<ChatMessage> messages)
        {
            var requestBody = new
            {
                model = "qwen-turbo",
                messages = messages,
                stream = false
            };

            var json = JsonSerializer.Serialize(requestBody);
            var content = new StringContent(json, Encoding.UTF8, "application/json");
            var response = await client.PostAsync(url, content);
            var responseString = await response.Content.ReadAsStringAsync();

            if (!response.IsSuccessStatusCode)
            {
                throw new Exception($"API调用失败: {response.StatusCode}");
            }

            return ParseChatResponse(responseString);
        }

        /// <summary>
        /// 解析普通Chat响应
        /// </summary>
        private ChatResponse ParseChatResponse(string jsonString)
		{
		    var doc = JsonDocument.Parse(jsonString);
		    var root = doc.RootElement;
		    var firstChoice = root.GetProperty("choices")[0];
		
		    var result = new ChatResponse();
		    var messageElement = firstChoice.GetProperty("message");
		    result.Message = new ChatMessage(
		        messageElement.GetProperty("role").GetString(),
		        messageElement.GetProperty("content").GetString() ?? ""
		    );
		    result.FinishReason = firstChoice.GetProperty("finish_reason").GetString();
		
		    return result;
		}
}

/// <summary>
        /// 2. 设置系统提示词(可选,放在历史第一位)
        /// </summary>
        public void SetSystemPrompt(string systemPrompt)
        {
            // 如果已经有system消息,先移除
            if (_history.Count > 0 && _history[0].Role == "system")
            {
                _history.RemoveAt(0);
            }
            // 插入到最前面
            _history.Insert(0, new ChatMessage("system", systemPrompt));
        }

/// <summary>
/// 工具定义:获取当前时间
/// </summary>
private object GetCurrentTimeTool()
{
    return new
    {
        type = "function",
        function = new
        {
            name = "get_current_time",
            description = "获取当前的日期和时间。用户询问当前时间、现在几点时必须调用此函数。",
            parameters = new { }
        }
    };
}


// 在 CommonHelper 类中
public string ExecuteTool(string toolName, string arguments)
{
    switch (toolName)
    {
        case "get_current_time":
            return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        // 未来添加更多工具
        default:
            return $"未知工具: {toolName}";
    }
}

2. 辅助类

csharp 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace ConsoleApp1.Model
{
    //消息体
    public class ChatMessage
    {
        public ChatMessage() { }
        public ChatMessage(string role, string content)
        {
            Role = role;
            Content = content;
        }

        /// <summary>
        /// 消息角色:system / user / assistant
        /// </summary>
        [JsonPropertyName("role")]
        public string Role { get; set; } = string.Empty;

        /// <summary>
        /// 消息内容
        /// </summary>
        [JsonPropertyName("content")]
        public string Content { get; set; } = string.Empty;


        [JsonPropertyName("tool_call_id")]
        public string ToolCallId { get; set; }  //用于tool角色消息


        [JsonPropertyName("tool_calls")]
        public List<ToolCall> ToolCalls { get; set; }  // 🆕 用于assistant消息



        public override string ToString()
        {
            return $"{Role}: {Content}";
        }

        

    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Text.Json.Serialization;


namespace ConsoleApp1.Model
{
    public class ChatResponse
    {
        [JsonPropertyName("choices")]
        public List<Choice> Choices { get; set; } = new List<Choice>();

        [JsonPropertyName("finish_reason")]
        public string FinishReason { get; set; } = "";


        [JsonPropertyName("Message")]
        public ChatMessage Message { get; set; } = new ChatMessage();


    }

    public class Choice
    {
        [JsonPropertyName("message")]
        public ChatMessage Message { get; set; } = new ChatMessage();

        [JsonPropertyName("finish_reason")]
        public string FinishReason { get; set; } = "";
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1.Model
{
    public class FunctionCall
    {
        public string Name { get; set; } = "";
        public string Arguments { get; set; } = "";
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1.Model
{
    public class FunctionCallResponse
    {
        public string FinishReason { get; set; } = "";
        public ChatMessage Message { get; set; } = new ChatMessage();
        public List<ToolCall> ToolCalls { get; set; } = new List<ToolCall>();

    }
}

3.调用

csharp 复制代码
Console.WriteLine($"FunctionCalling Study\r\n");
CommonClass client = new CommonClass();
client.SetSystemPrompt("你是一个智能助手。当用户询问时间、日期时,必须调用 get_current_time 函数获取真实时间,不要自己编造。");
var tools = new object[] { GetCurrentTimeTool() };
// 测试1:查询时间
Console.WriteLine("用户:现在几点了? 当前时间");
var reply1 = await client.CallAPIWithTools("现在几点了?", tools);
Console.WriteLine($"AI回答(1):{reply1}\n");

4. 运行效果

用户:现在几点了?

AI回答:现在是下午2点30分25秒

5. 常见问题

Q1:第1次请求没有返回tool_calls,而是直接回答了?

原因:AI认为不需要调用工具(比如用户没问时间)

解决:正常情况,直接返回AI的回答即可。

Q2:返回了tool_calls但函数名不对?

原因:工具定义中的name和description不够清晰

解决:确保description让AI能准确理解什么时候该调用。

Q3:第2次请求后AI还是返回工具调用?

原因:工具结果不够详细,AI还需要更多信息

解决:检查工具返回的内容是否清晰。

Q4:第一次调用AI直接输出了错误的时间

原因:说明AI认为自己可以直接回答时间问题,不需要调用工具。AI的训练数据中包含了大量"当前时间"的示例,但它回答的是训练数据中的时间,不是真实时间。

解决方法:明确设置提示词:"你是一个智能助手。当用户询问时间、日期时,必须调用 get_current_time 函数获取真实时间,不要自己编造。"

相关推荐
上海锝秉工控1 分钟前
省线型增量编码器:用“减法思维“重构工业控制的未来
网络·人工智能·重构
蓝星空20001 分钟前
怎么使用 Image 2 高效生成商业级 AI 图像(GPT-Image-2 全流程实操教程)
人工智能·gpt·ai作画
沉下去,苦磨练!3 分钟前
张量的形状操作以及拼接
人工智能
小黄人软件7 分钟前
Claude和Codex下载离线包 安装遇到问题:windows无法访问指定设备 路径 文件 应用无法打开也无法卸载,解决了
人工智能·microsoft·openai·codex
落叶无情7 分钟前
分析:不上传文档给AI上下文窗口,仅让Ai上网搜索icef认知框架的详情,可以获得比较完整的信息,并可直接进行基本推理的具体机制
人工智能
Hector_zh10 分钟前
逐浪 · 第十一篇: Vibe Coding 下的效率定义与规范建设
人工智能·vibecoding
147API16 分钟前
Claude进入受监管系统前,接入层应该先怎么设计
人工智能
Szime17 分钟前
深智微:面向汽车电子与工业控制的电子元器件原装现货服务商
人工智能·汽车
gis分享者18 分钟前
Claude Code 接入蓝耘 GLM-5.1:终端 AI 编程助手配置实战
人工智能·ai·实战·claude·cc·接入glm
东方隐侠安全团队-千里21 分钟前
币安Skills Hub:散户的“机构级超能力“来了
安全·ai·区块链·skills