上一集我们用 C# 连上 DeepSeek,跑通了一个会讲笑话的 Agent。那一集里,Agent 的能耐就是聊天:你问一句,它回一段,仅此而已。
这一集往前走一步:让 Agent 能调用你自己写的 C# 函数。行话叫"函数调用"(function calling)或者"工具调用"(tool calling)。有了它,Agent 就不只是动嘴,而是能真的去查数据、算结果、调接口。
示例源码位于:
dotnet/samples/01-get-started/02_add_tools/
一、这一集比第一集多了什么
第一集的代码只有三块:建客户端、造 Agent、调用。第二集在中间插了一样东西,就是"工具"。具体多了三处:
- 一个普通的 C# 静态方法
GetWeather,它会被当成工具交给 Agent; [Description]特性,用来告诉模型这个方法干嘛用、参数什么意思;AIFunctionFactory.Create把方法包成 MAF 认得的工具,再传给AsAIAgent的tools参数。
除了这几点,建客户端和调用的写法和第一集完全一样。
二、先搞懂:什么是函数调用
大模型本身只会"接着你的话往下编文字"。它不知道现在的真实天气,也不会算数。函数调用解决的就是这个问题。
思路是这样的:你提前把几个 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 的工具对象,放进AsAIAgent的tools数组。这是第二集相对第一集唯一的结构性改动。- 后面的
RunAsync和RunStreamingAsync跟第一集写法一样,区别在内部:这一回 Agent 可能在回答前先悄悄调一次你的工具。
五、运行时到底发生了什么
把"阿姆斯特丹现在天气怎么样?"这一句拆开看,幕后其实走了五步:
- 你的程序把问题连同工具定义(也就是那两段
[Description])一起发给 DeepSeek; - DeepSeek 判断:这个问题用
GetWeather能答,于是返回一个调用请求,而不是最终答案,参数location = "阿姆斯特丹"; - MAF 在你的程序里执行
GetWeather("阿姆斯特丹"),拿到"阿姆斯特丹 当前多云,最高气温 15°C。"; - MAF 把这句结果再发给 DeepSeek;
- 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_KEY 和 DEEPSEEK_MODEL。 |
| 想让工具带多个参数 | 方法签名太简单 | 给方法加参数,每个都补 [Description] 即可。 |
| 工具返回中文,模型却用英文答 | 没要求中文 | 在 instructions 里加一句"请用中文回答。" |
八、动手改造
跑通之后,试试这几件事,能帮你把函数调用吃透:
- 加第二个工具:再写一个
GetTime(string city),让 Agent 也能报时,然后问它"现在北京几点"。 - 改描述观察变化:把
GetWeather的描述改得更具体或更模糊,看模型调用得准不准。 - 让 Agent 组合工具:问"阿姆斯特丹和北京天气差多少",看它会不会分别调用两次
GetWeather,再自己算差值。
九、小结
这一集你学会了函数调用:用 [Description] 给 C# 方法写说明,用 AIFunctionFactory.Create 把它变成工具,再交给 AsAIAgent 的 tools 参数。模型决定什么时候调、传什么参,你的代码在本地执行并回传结果,模型最后组织成回答。到这一步,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)