老胡
发布于 2026-09-24 / 2 阅读
0
0

MAF 快速上手(2)给 Agent 装上"工具":函数调用Function Call

上一集我们用 C# 连上 DeepSeek,跑通了一个会讲笑话的 Agent。那一集里,Agent 的能耐就是聊天:你问一句,它回一段,仅此而已。

这一集往前走一步:让 Agent 能调用你自己写的 C# 函数。行话叫"函数调用"(function calling)或者"工具调用"(tool calling)。有了它,Agent 就不只是动嘴,而是能真的去查数据、算结果、调接口。

示例源码位于:dotnet/samples/01-get-started/02_add_tools/


一、这一集比第一集多了什么

第一集的代码只有三块:建客户端、造 Agent、调用。第二集在中间插了一样东西,就是"工具"。具体多了三处:

  1. 一个普通的 C# 静态方法 GetWeather,它会被当成工具交给 Agent;
  2. [Description] 特性,用来告诉模型这个方法干嘛用、参数什么意思;
  3. AIFunctionFactory.Create 把方法包成 MAF 认得的工具,再传给 AsAIAgenttools 参数。

除了这几点,建客户端和调用的写法和第一集完全一样。


二、先搞懂:什么是函数调用

大模型本身只会"接着你的话往下编文字"。它不知道现在的真实天气,也不会算数。函数调用解决的就是这个问题。

思路是这样的:你提前把几个 C# 方法登记给 Agent,并写清楚每个方法能干什么。用户提问时,如果模型觉得某个方法能帮忙,它不会自己瞎编答案,而是返回一个"请调用这个方法,参数是这些"的请求。你的程序真的执行那个方法,拿到结果,再把结果交给模型,模型最后用自然语言把答案说给你听。

拿本集的例子说:你问"阿姆斯特丹天气怎么样",模型看到你挂了个 GetWeather 工具,就决定调用它,参数是 location = "阿姆斯特丹"。你的 GetWeather 方法跑完,返回"阿姆斯特丹 当前多云,最高气温 15°C。",模型读到这句,再组织成一段人话回复你。

这里有一个容易绕晕的点:天气不是模型算出来的,是你的方法算出来的。模型只负责"判断该用哪个工具、填什么参数",真正的活儿在你本地代码里。


三、环境(简版,详细看第一集)

和第一集一模一样,两个环境变量:

变量名必填说明默认值
DEEPSEEK_API_KEY必填DeepSeek 密钥无,缺失程序会报错退出
DEEPSEEK_MODEL可选模型名deepseek-flash

设置好之后直接 dotnet run 即可,无需新装任何东西。


四、核心代码逐行讲

// Copyright (c) Microsoft. All rights reserved.

// 本示例演示如何用 OpenAI 兼容接口(这里用 DeepSeek)创建一个带函数工具的 Agent,
// 并分别以非流式和流式两种方式与其交互,工具为一个天气查询函数。

using System.ClientModel;
using System.ComponentModel;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Chat;

// DeepSeek 提供 OpenAI 兼容的接口。密钥通过环境变量提供;
// 默认模型为 "deepseek-flash"(官方当前推荐)。其他可选模型包括 "deepseek-v4-pro"。
// 注意:旧模型名如 "deepseek-chat" / "deepseek-reasoner" 已不在官方文档中,应避免使用。
var apiKey = Environment.GetEnvironmentVariable("DEEPSEEK_API_KEY") ?? throw new InvalidOperationException("未设置环境变量 DEEPSEEK_API_KEY。");
var model = Environment.GetEnvironmentVariable("DEEPSEEK_MODEL") ?? "deepseek-flash";

// 一个可供 Agent 调用的函数工具。[Description] 特性告诉模型这个函数做什么、
// 参数表示什么,模型据此决定何时以及如何调用它。
[Description("获取指定地点的天气。")]
static string GetWeather([Description("要查询天气的地点。")] string location)
    => $"{location} 当前多云,最高气温 15°C。";

// 将 OpenAI 客户端指向 DeepSeek 的接口地址,而不是 api.openai.com。
OpenAIClient openAIClient = new(
    new ApiKeyCredential(apiKey),
    new OpenAIClientOptions { Endpoint = new Uri("https://api.deepseek.com") });

// 获取选定模型的聊天客户端,并将其包装为 MAF 的 Agent,同时把工具交给它。
ChatClient chatClient = openAIClient.GetChatClient(model);
AIAgent agent = chatClient.AsAIAgent(
    instructions: "你是一个乐于助人的助手。",
    tools: [AIFunctionFactory.Create(GetWeather)]);

// 非流式调用(带函数工具)的 Agent 交互。
Console.WriteLine(await agent.RunAsync("阿姆斯特丹现在天气怎么样?"));

// 流式调用(带函数工具)的 Agent 交互。
await foreach (var update in agent.RunStreamingAsync("阿姆斯特丹现在天气怎么样?"))
{
    Console.WriteLine(update);
}

几个值得停下来看的地方:

  • GetWeather 是个再普通不过的静态方法,入参 location,返回一段字符串。真正让它变成"工具"的不是方法体,而是上面的两处 [Description]
  • [Description] 是写给模型看的说明书。方法上的那句告诉模型"这个工具能干嘛",参数上的那句告诉模型"这个参数该填什么"。模型就是靠这些文字来决定要不要调用、怎么填参。所以描述要写清楚、写自然,含糊的描述会让模型调错或干脆不调。
  • AIFunctionFactory.Create(GetWeather) 把方法包成 MAF 的工具对象,放进 AsAIAgenttools 数组。这是第二集相对第一集唯一的结构性改动。
  • 后面的 RunAsyncRunStreamingAsync 跟第一集写法一样,区别在内部:这一回 Agent 可能在回答前先悄悄调一次你的工具。

五、运行时到底发生了什么

把"阿姆斯特丹现在天气怎么样?"这一句拆开看,幕后其实走了五步:

  1. 你的程序把问题连同工具定义(也就是那两段 [Description])一起发给 DeepSeek;
  2. DeepSeek 判断:这个问题用 GetWeather 能答,于是返回一个调用请求,而不是最终答案,参数 location = "阿姆斯特丹"
  3. MAF 在你的程序里执行 GetWeather("阿姆斯特丹"),拿到"阿姆斯特丹 当前多云,最高气温 15°C。";
  4. MAF 把这句结果再发给 DeepSeek;
  5. DeepSeek 据此生成自然语言回答,返回给你。

所以整件事里,天气逻辑是你在本地跑的,模型只当了"调度员"。


六、运行与结果

cd d:\gitee\agent-framework-main\dotnet\samples\01-get-started\02_add_tools
dotnet run

控制台会先打印普通调用的回答,再流式打印一遍。你会看到 Agent 用中文告诉你阿姆斯特丹的天气,而这个数字来自你自己的 GetWeather 方法,不是模型编的。流式输出时,工具调用的中间过程也可能夹在更新里一起打印出来。


七、常见问题

现象可能原因处理建议
Agent 没调工具,自己编了个天气[Description] 太含糊,或问题本身不需要工具把方法描述写具体;问一个明确要查天气的问题,比如"阿姆斯特丹天气怎样"。
报 401 或模型错误Key 错或模型名不对同第一集,检查 DEEPSEEK_API_KEYDEEPSEEK_MODEL
想让工具带多个参数方法签名太简单给方法加参数,每个都补 [Description] 即可。
工具返回中文,模型却用英文答没要求中文instructions 里加一句"请用中文回答。"

八、动手改造

跑通之后,试试这几件事,能帮你把函数调用吃透:

  • 加第二个工具:再写一个 GetTime(string city),让 Agent 也能报时,然后问它"现在北京几点"。
  • 改描述观察变化:把 GetWeather 的描述改得更具体或更模糊,看模型调用得准不准。
  • 让 Agent 组合工具:问"阿姆斯特丹和北京天气差多少",看它会不会分别调用两次 GetWeather,再自己算差值。

九、小结

这一集你学会了函数调用:用 [Description] 给 C# 方法写说明,用 AIFunctionFactory.Create 把它变成工具,再交给 AsAIAgenttools 参数。模型决定什么时候调、传什么参,你的代码在本地执行并回传结果,模型最后组织成回答。到这一步,Agent 才真正从"聊天机器人"变成"能动手干活"的角色。

下一篇我们讲多轮对话(03_multi_turn),让 Agent 记住上下文,连续聊下去。

示例源码

  • 本示例:dotnet/samples/01-get-started/02_add_tools/Program.cs
  • 上一篇:dotnet/samples/01-get-started/01_hello_agent/使用说明书.md

参考资料

  • Microsoft Agent Framework 的 OpenAI 扩展(Microsoft.Agents.AI.OpenAI
  • DeepSeek 官方 API 文档(中文):https://api-docs.deepseek.com/zh-cn/
  • 参考博文:《MAF快速入门(1)化繁为简的AGENT创建范式》— EdisonZhou(cnblogs)

评论