19-AIAgent智能代理开发

AI Agent 的吸引力在于它不只是回答问题,而是会规划步骤、调用工具并根据反馈继续执行任务。这一篇会把 Agent 的核心组成拆开来看,帮助你理解它和普通聊天调用的差别,以及在 .NET 项目中怎样逐步搭出一个能工作的代理原型。

很多人第一次接触 AI Agent,都会把它和聊天机器人混为一谈。实际上,Agent 的重点并不是"更会聊天",而是"能围绕目标持续行动":它会理解任务、决定下一步、调用工具、记录状态,必要时还会根据结果修正策略。本篇会用 .NET 的视角把这些能力拆开讲,并通过插件、函数调用和任务编排示例,带你写出一个真正有执行味道的智能代理雏形。

一、先把Agent和普通聊天模型区分开

进入"一、先把Agent和普通聊天模型区分开"之前,可以先把这一节理解成整篇内容里的一个关键台阶。下面会先说明它在实际项目中的作用,再结合示例代码解释它为什么值得你现在就掌握。

1.1 Agent的本质是目标、工具和状态的组合

进入"1.1 Agent的本质是目标、工具和状态的组合"之前,可以先把这一节理解成整篇内容里的一个关键台阶。下面会先说明它在实际项目中的作用,再结合示例代码解释它为什么值得你现在就掌握。

如果只让模型回答一句话,它更像对话助手;如果让模型围绕一个目标持续工作,它才开始接近 Agent。这里面最关键的,不是"模型参数更大",而是系统设计发生了变化。Agent 至少要知道当前目标是什么、自己能用哪些工具、已经做到了哪一步,以及下一步准备怎么继续推进。

也就是说,Agent 并不是一个新的模型品类,而是一种把大模型嵌入任务执行流程中的编排方式。模型负责理解和决策,工具负责接触外部世界,状态负责让多轮执行保持连续性。三者缺一不可。没有工具,Agent 只能空谈;没有状态,Agent 每一轮都像失忆;没有目标,Agent 就很容易在长任务里跑偏。

1.2 为什么代理任务通常需要规划和反思

进入"1.2 为什么代理任务通常需要规划和反思"之前,可以先把这一节理解成整篇内容里的一个关键台阶。下面会先说明它在实际项目中的作用,再结合示例代码解释它为什么值得你现在就掌握。

一旦任务从"回答一个问题"变成"完成一件事",中间就会自然出现拆解步骤、检查结果和调整路线的需要。比如"帮我整理今天的待办并安排优先级",它不是一句话就能结束,而是需要先读取现有数据,再分析紧急程度,再生成安排建议,最后把结果写回系统。

text 复制代码
接收目标
   ↓
判断是否需要工具
   ↓
选择工具并执行
   ↓
读取执行结果
   ↓
判断任务是否完成
   ↓
未完成则继续下一轮,必要时修正计划

这个循环说明了 Agent 和普通问答最大的差别:它会在"思考"和"行动"之间来回切换。你在设计 Agent 时,不应该只考虑提示词怎么写,还要考虑工具边界、状态持久化、失败时如何恢复。真正实用的 Agent,往往是一个受控的任务执行器,而不是一个无限放权的自由聊天体。

二、先让模型学会调用你的工具

进入"二、先让模型学会调用你的工具"之前,可以先把这一节理解成整篇内容里的一个关键台阶。下面会先说明它在实际项目中的作用,再结合示例代码解释它为什么值得你现在就掌握。

2.1 用Kernel插件暴露可执行能力

进入"2.1 用Kernel插件暴露可执行能力"之前,可以先把这一节理解成整篇内容里的一个关键台阶。下面会先说明它在实际项目中的作用,再结合示例代码解释它为什么值得你现在就掌握。

Agent 之所以能做事,不是因为它凭空长出了手脚,而是因为你把系统里的可执行能力以工具的形式暴露给了模型。在 .NET 生态里,Semantic Kernel 提供了一种很适合教学和原型开发的做法:用插件类加上 KernelFunction 标记,把普通方法注册成模型可调用的工具。

csharp 复制代码
using System.ComponentModel;
using Microsoft.SemanticKernel;

public sealed class WorkbenchPlugin
{
    private readonly List<string> _todos = new();

    [KernelFunction("add_todo")]
    [Description("添加一条待办事项")]
    public string AddTodo(string content)
    {
        _todos.Add(content);
        return $"已添加待办:{content}";
    }

    [KernelFunction("list_todos")]
    [Description("列出所有待办事项")]
    public string ListTodos()
    {
        if (_todos.Count == 0)
        {
            return "当前没有待办事项。";
        }

        return string.Join(Environment.NewLine, _todos.Select((todo, index) => $"{index + 1}. {todo}"));
    }

    [KernelFunction("finish_todo")]
    [Description("根据编号完成待办事项,编号从 1 开始")]
    public string FinishTodo(int number)
    {
        if (number < 1 || number > _todos.Count)
        {
            return "编号不存在。";
        }

        var removed = _todos[number - 1];
        _todos.RemoveAt(number - 1);
        return $"已完成待办:{removed}";
    }
}

这个插件类虽然简单,却完整体现了工具设计的几个要点。第一,每个方法都应该职责单一,避免一个工具又查又改又发通知,导致模型难以稳定选择。第二,方法描述要写得清楚,因为模型会根据这些描述来判断什么时候该调用哪个函数。第三,返回值尽量是结构明确、容易读懂的文本,方便模型把工具结果纳入后续推理。

很多初学者一上来就想给 Agent 接几十个工具,结果模型频繁误调用。更实际的做法,是像这里一样从小而清晰的工具集开始。只有当一个工具的输入输出、权限边界和失败提示都足够明确时,它才适合交给 Agent 自动调用。

2.2 打开函数调用,让模型在对话里自动选工具

进入"2.2 打开函数调用,让模型在对话里自动选工具"之前,可以先把这一节理解成整篇内容里的一个关键台阶。下面会先说明它在实际项目中的作用,再结合示例代码解释它为什么值得你现在就掌握。

把插件注册进去之后,下一步才是让模型"有机会"调用这些工具。Semantic Kernel 的常见做法,是通过执行设置启用自动函数调用。这样模型在收到任务时,会先判断需不需要用工具,再由内核自动执行对应方法,把结果回填给模型继续生成回复。

csharp 复制代码
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;

var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")
             ?? throw new InvalidOperationException("请先设置 OPENAI_API_KEY 环境变量。");

var kernel = Kernel.CreateBuilder()
    .AddOpenAIChatCompletion(modelId: "gpt-4o-mini", apiKey: apiKey)
    .Build();

kernel.Plugins.AddFromType<WorkbenchPlugin>();

var settings = new OpenAIPromptExecutionSettings
{
    ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
};

var prompt = "请先添加待办"准备周会材料"和"整理客户反馈",然后列出当前待办。";
var result = await kernel.InvokePromptAsync(prompt, new(settings));

Console.WriteLine(result);

在这段代码里,kernel.Plugins.AddFromType<WorkbenchPlugin>() 表示把刚才定义的工具集注册到内核中。OpenAIPromptExecutionSettings 则告诉模型:如果你认为某个函数能帮助完成当前任务,可以自动调用。这里的关键参数 ToolCallBehavior.AutoInvokeKernelFunctions,本质上就是在开启"模型决策、系统执行、再把结果返回给模型"的循环。

这和单纯 InvokePromptAsync 的最大区别在于,模型不再只是输出一段自然语言,而是拥有了"先做事再回答"的机会。你可以把它理解为 Agent 的第一步:从会说,变成会借助系统能力行动。当然,自动函数调用不等于完全放权。凡是涉及付费、删除、外发消息等高风险操作,真实项目里都建议增加确认流程或权限校验。

三、让Agent具备连续执行任务的能力

进入"三、让Agent具备连续执行任务的能力"之前,可以先把这一节理解成整篇内容里的一个关键台阶。下面会先说明它在实际项目中的作用,再结合示例代码解释它为什么值得你现在就掌握。

3.1 用任务状态把多轮执行串起来

进入"3.1 用任务状态把多轮执行串起来"之前,可以先把这一节理解成整篇内容里的一个关键台阶。下面会先说明它在实际项目中的作用,再结合示例代码解释它为什么值得你现在就掌握。

当 Agent 开始能调工具之后,下一个问题就是:它如何知道自己已经做到哪一步?如果没有状态,Agent 每次收到输入都只能把当前这一轮当成独立对话,长任务会很快失去上下文。因此,在工程实践里,任务状态往往要作为一等公民单独管理。

csharp 复制代码
public sealed record AgentTaskState(
    string Goal,
    List<string> Observations,
    bool Completed);

public sealed class TaskOrchestrator
{
    private readonly Kernel _kernel;

    public TaskOrchestrator(Kernel kernel)
    {
        _kernel = kernel;
    }

    public async Task<string> RunAsync(AgentTaskState state, CancellationToken cancellationToken = default)
    {
        var history = string.Join(Environment.NewLine, state.Observations);

        var prompt = $$"""
你是任务执行代理,请根据当前目标和历史观察决定下一步。
如果任务已经完成,请明确总结结果;如果未完成,请继续调用工具推进。

目标:{{state.Goal}}
历史观察:
{{history}}
""";

        var settings = new OpenAIPromptExecutionSettings
        {
            ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
        };

        var result = await _kernel.InvokePromptAsync(prompt, new(settings), cancellationToken: cancellationToken);
        state.Observations.Add(result.ToString());
        return result.ToString();
    }
}

这段代码刻意没有追求复杂,而是想把状态驱动的核心思路讲清楚。AgentTaskState 记录了目标、观察记录和是否完成,等于给 Agent 一份持续更新的工作备忘录。TaskOrchestrator 每执行一轮,就把现有状态整理成提示词,让模型在"已有结果"的基础上决定下一步,而不是每次从零开始重新猜。

在真实系统里,这个状态完全可以落到数据库、缓存或工作流引擎中,而不是只放在内存里。你甚至可以给它增加 CurrentStepLastToolCallRetryCount 之类的字段,让 Agent 对失败重试、人工接管、长任务恢复都有更强的可控性。Agent 的可靠性,很多时候并不取决于模型有多聪明,而是取决于状态是否清晰、可追踪、可恢复。

3.2 多代理协作时,重点不是数量,而是角色边界

进入"3.2 多代理协作时,重点不是数量,而是角色边界"之前,可以先把这一节理解成整篇内容里的一个关键台阶。下面会先说明它在实际项目中的作用,再结合示例代码解释它为什么值得你现在就掌握。

多 Agent 协作很容易让人兴奋,因为看起来像"让一群 AI 团队开会"。但真正做项目时,更重要的问题是:每个代理负责什么,交付什么,什么时候该轮到下一个代理接手。角色边界如果不清楚,多代理只会把原本简单的问题放大成更多轮无效对话。

csharp 复制代码
public sealed record AgentRole(string Name, string Responsibility);

var roles = new[]
{
    new AgentRole("需求分析代理", "把用户目标拆成清晰步骤,识别需要哪些工具"),
    new AgentRole("执行代理", "调用工具完成任务,并记录执行结果"),
    new AgentRole("审查代理", "检查输出是否遗漏、是否违反约束、是否需要人工确认")
};

虽然这段代码只是一个角色定义示意,但它反映了多 Agent 设计的正确切入点:先想清楚流程分工,再谈如何让它们对话。一个比较稳妥的做法,是让第一个代理负责规划,第二个代理负责执行,第三个代理负责审查或收尾,而不是让所有代理都拥有相同权限和相同目标。分工明确之后,你甚至不一定需要复杂的"自动群聊机制",顺序编排往往就已经够用。

这也解释了为什么很多企业级 Agent 项目在初期并不会直接上多 Agent。单代理加清晰工具,先把核心链路做稳定,再考虑拆角色,通常更容易控制风险。只有当任务本身确实跨越多个专业领域,或者审查链路有强合规要求时,多 Agent 的收益才会明显超过它带来的复杂度。

四、Agent真正进入业务系统后要注意什么

进入"四、Agent真正进入业务系统后要注意什么"之前,可以先把这一节理解成整篇内容里的一个关键台阶。下面会先说明它在实际项目中的作用,再结合示例代码解释它为什么值得你现在就掌握。

4.1 权限、确认和幂等性必须先于"更聪明"

进入"4.1 权限、确认和幂等性必须先于"更聪明""之前,可以先把这一节理解成整篇内容里的一个关键台阶。下面会先说明它在实际项目中的作用,再结合示例代码解释它为什么值得你现在就掌握。

一旦 Agent 具备了调用工具、修改数据乃至触发外部操作的能力,系统风险就会迅速上升。很多人最先关注的是"模型会不会选错工具",但更值得优先处理的是:即使选对了工具,它有没有权限这么做?这个操作是否需要用户确认?如果同一个动作被重复触发两次,会不会造成重复扣费、重复发信或重复创建订单?

因此,Agent 系统在工程上必须把权限和幂等性纳入设计。高风险工具应要求明确确认,关键写操作最好带业务幂等键,工具层也应该自己校验参数,不要把安全性完全寄托在模型是否足够谨慎。模型是决策者之一,但不是最终的安全边界。

4.2 可观测性和失败恢复决定了系统能否长期维护

进入"4.2 可观测性和失败恢复决定了系统能否长期维护"之前,可以先把这一节理解成整篇内容里的一个关键台阶。下面会先说明它在实际项目中的作用,再结合示例代码解释它为什么值得你现在就掌握。

Agent 出问题时,最怕的是你根本不知道它哪里出错了。是提示词理解偏了?是工具调用失败?还是工具明明执行成功,但结果没被模型正确吸收?所以,Agent 系统一定要保留足够多的执行日志,包括每一轮输入、每一次工具调用、工具返回结果、最终输出和耗时情况。

失败恢复也同样关键。长任务不可能保证每一步都顺利,如果一个外部接口临时超时,理想做法不应该是整条任务直接报废,而是能够重试、跳过、等待人工处理,或者从上次状态恢复继续跑。你越把 Agent 当成一个真正的业务执行器,而不是一个演示用聊天机器人,就越能理解这些工程能力的重要性。

五、把Agent开发的主线重新收一下

进入"五、把Agent开发的主线重新收一下"之前,可以先把这一节理解成整篇内容里的一个关键台阶。下面会先说明它在实际项目中的作用,再结合示例代码解释它为什么值得你现在就掌握。

5.1 从"会聊天"走向"会执行"的关键在系统设计

进入"5.1 从"会聊天"走向"会执行"的关键在系统设计"之前,可以先把这一节理解成整篇内容里的一个关键台阶。下面会先说明它在实际项目中的作用,再结合示例代码解释它为什么值得你现在就掌握。

本篇想让你真正建立起来的,不只是几个 Semantic Kernel API 的印象,而是对 Agent 结构的整体认识:Agent 要围绕目标持续工作,因此需要工具、状态和执行循环;工具要小而清晰;函数调用让模型有机会先行动再回答;状态管理让多轮任务可持续;权限与日志则让整个系统可控、可追踪。

当你继续往下做更复杂的工作流、企业助手或自动化处理系统时,最重要的不是让模型显得多聪明,而是让整个 Agent 链路足够稳定、边界足够清楚。只有这样,Agent 才会从"看上去很厉害的演示"真正变成"可以接进业务系统的执行能力"。

练习题:

  1. 请解释 AI Agent 与普通聊天调用最大的结构差异是什么。
  2. 如果你要让 Agent 帮你管理个人日程,你会优先提供哪三类工具函数,为什么?
  3. 请思考多 Agent 协作的收益与代价,并说明什么场景下不值得引入多代理。
相关推荐
麦聪聊数据3 小时前
SQL 到 API 转化过程中的版本控制与灰度发布机制
数据库·sql·低代码·微服务
唐青枫4 小时前
深入理解 C#.NET TaskScheduler:为什么大量使用 Work-Stealing
c#·.net
喵叔哟5 小时前
20-多模态AI应用开发
人工智能·微服务·.net
桑榆肖物5 小时前
.NET 10 Native AOT 在 Linux 嵌入式设备上的实战
java·linux·.net·aot
fajianchen6 小时前
如何设计微服务统一认证中心
微服务·云原生·架构·iam
我是唐青枫6 小时前
深入理解 C#.NET Task.Run:调度原理、线程池机制与性能优化
性能优化·c#·.net
步步为营DotNet6 小时前
深入剖析.NET 11中Microsoft.Extensions.AI的应用与优化 前言
人工智能·microsoft·.net
江沉晚呤时6 小时前
基于 AssemblyLoadContext 的 .NET 插件化架构设计与实现
开发语言·c#·.net