Agent Framework(1):会聊天的模型不值钱:微软用 7 步,把 LLM 从嘴替做成能上岗的 Agent

先别写代码:这套东西到底是什么

Agent Framework 是一套开源开发工具,用来做 AI agentmulti-agent workflow ,语言主场是 .NET 和 Python 。Go 有端口,但官方写明仍是 public preview ;声明式 agent、RAG、CodeAct、函数式工作流在 Go 上还没有。问题往 agent-framework-go 的 Issues 提。

它不是从零发明的新宗教。同一批人把 AutoGen 那套「agent 抽象简单、能协作」和 Semantic Kernel 那套「会话、类型安全、中间件、遥测、企业集成」并到一起,又补上图工作流。官方的原话是:这是两者往后的统一底座。

模型后端不绑死一家。Overview 里点名的有 Microsoft Foundry、Anthropic、Azure OpenAI、OpenAI、Ollama。入门教程默认走 Foundry,只是因为示例用 AIProjectClient / FoundryChatClient 最省事,不是框架边界。

还有一句比任何 API 都值钱的话,写在 Overview 的选择表下面:

If you can write a function to handle the task, do that instead of using an AI agent.

能写成普通函数的事,别上 agent。后面 Workflow 和 Harness 都是在这个判断之上往上加,不是往下替代。

安装也很直:

  • .NET:dotnet add package Microsoft.Agents.AI.Foundry --prerelease,外加 Azure.AI.ProjectsAzure.Identity
  • Python:pip install agent-framework(入门示例常再加 azure-identity
  • Go:go get github.com/microsoft/agent-framework-go

官方把「用 agent」和「用 workflow」分得很干脆:

用 agent 的时候 用 workflow 的时候
任务开放、偏对话 过程步骤已经写清楚
需要模型自己决定调不调工具、怎么规划 你要亲自控制执行顺序
一次 LLM 调用(加上工具)就够 多个 agent 或函数必须协同

框架的最小单元:连上模型的 Agent

框架里的 agent,首先是「带说明书的模型客户端」。C# 里常写成 AIProjectClient.AsAIAgent(...)。Python 是 FoundryChatClientAgent(...),Go 是 foundryprovider.NewAgent。三种写法都在构造同一件事:哪颗模型、用什么证件、开口前读哪段 instructions、这个对象叫什么名字。

csharp 复制代码
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;

var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
    ?? throw new InvalidOperationException("Set AZURE_OPENAI_ENDPOINT");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";

AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
    .AsAIAgent(
        model: deploymentName,
        instructions: "You are a friendly assistant. Keep your answers brief.",
        name: "HelloAgent");

Console.WriteLine(await agent.RunAsync("What is the largest city in France?"));

AIProjectClient 是连接。instructions 是每次调用都会带上的系统说明。name 在这一层只是标识,托管之后会变成路由里的名字。RunAsync 把这一句用户输入送进去,等到完整回答。默认不带上一轮对话,也没有工具。所以这一层能做问答,不能办事,也不能连续聊。

同一层还有流式出口。长任务和 Harness 都靠它把中间过程打出来。

csharp 复制代码
await foreach (var update in agent.RunStreamingAsync("Tell me a one-sentence fun fact."))
{
    Console.Write(update);
}

证件示例用 DefaultAzureCredential。文档写明生产要换成明确的托管身份,避免探测延迟和拿到错误身份。后面所有工具、session、Harness、Hosting,操作的都还是这个 AIAgent


框架怎样让 Agent 调用你的代码

第二层是工具。框架不自己决定「该不该查天气」。它把你的函数登记成模型能看见的 schema,模型点名之后,运行时再执行。和自己解析 Chat Completions 的 tool call 不同,函数调用被收进 agent 对象里,一次 RunAsync 会把选择、执行、回填做完。

csharp 复制代码
using System.ComponentModel;
using Microsoft.Extensions.AI;

[Description("Get the weather for a given location.")]
static string GetWeather(
    [Description("The location to get the weather for.")] string location)
    => $"The weather in {location} is cloudy with a high of 15°C.";

AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
    .AsAIAgent(
        model: deploymentName,
        instructions: "You are a helpful weather agent. Use the GetWeather tool to answer questions.",
        name: "WeatherAgent",
        tools: [AIFunctionFactory.Create(GetWeather)]);

Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?"));

一次 RunAsync 在这一层变成四步:AIFunctionFactory.Create 生成工具说明 → 模型选择是否调用 → 运行时执行函数 → 结果回到模型,再生成给用户的句子。[Description] 是给模型看的,写含糊了,模型可能不调。入门示例没有把批准打开;生产要对着敏感工具走批准,而不是让函数直接跑。

也可以只在这一次 run 上附加工具。基类 AgentRunOptions 几乎没有公共旋钮,要传类型专用的 ChatClientAgentRunOptions,里面的 ChatOptions 会和 agent 级配置合并后再交给 IChatClient

csharp 复制代码
var chatOptions = new ChatOptions { Tools = [AIFunctionFactory.Create(GetWeather)] };
Console.WriteLine(await agent.RunAsync(
    "What is the weather like in Amsterdam?",
    options: new ChatClientAgentRunOptions(chatOptions)));

MCP 也走同一层思路,入门七章还没展开。这一层补的是「行动」。还没有「同一场对话」。


框架怎样保存一场对话

第三层是 AgentSession。框架把多轮状态做成显式对象,而不是在每次 RunAsync 里让你自己拼 messages。

csharp 复制代码
AgentSession session = await agent.CreateSessionAsync();

Console.WriteLine(await agent.RunAsync("My name is Alice and I love hiking.", session));
Console.WriteLine(await agent.RunAsync("What do you remember about me?", session));

同一条 session 传入第二次 RunAsync 时,第一次的问答会作为历史一起送走。换一条,就是一次新的独立调用。

在框架的划分里,session 管的是这场对话还在不在。文档没说默认 session 能活过进程重启,也没说它等于长期记忆。历史变长后会被截断。所以框架又拆出下一层:不把「每次开口都必须成立的事实」只放在聊天记录里。


框架怎样做记忆:历史和上下文是两条路

第四层最容易和 session 缠在一起。三者管的不是同一种数据。

Session 是这场对话的容器。没有它,每次 RunAsync 都是孤立请求;有了它,历史和会话状态才有地方放。它本身不规定「记什么」。C# 的 AgentSession 上有 StateBag,跨 run 的自定义数据放这里。

History provider 记的是消息:谁说了哪一句。服务若不自己托管对话,框架默认把完整历史放在 session 的内存里,也可以换成自定义 ChatHistoryProvider。它回答的问题是:下一轮模型还能不能看见刚才的原话。窗口满了,这些原话可以被截掉。

Context provider 记的是抽出来的事实,以及这些事实怎么改写下一次的 instructions。C# 扩展点是 AIContextProvider。调用前走 InvokingAsync,调用后走 InvokedAsync。框架只给这两个时机,不负责理解「我是 Alice」。

所以同一句「我叫 Alice」会进三个地方,待遇不同。Session 让它属于这场对话。History 可能整句留着当上文。Context provider 认出后把名字留下,并在之后每一次 InvokingAsync 里把「请用 Alice 称呼」写进返回的 AIContext.Instructions------即使历史已经被截断。

csharp 复制代码
using System.Text;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

internal sealed class UserInfo
{
    public string? UserName { get; set; }
}

internal sealed class UserMemoryProvider : AIContextProvider
{
    public UserInfo UserInfo { get; set; } = new();

    public override ValueTask<AIContext> InvokingAsync(
        InvokingContext context,
        CancellationToken cancellationToken = default)
    {
        var instructions = UserInfo.UserName is null
            ? "You don't know the user's name yet. Ask for it politely."
            : $"The user's name is {UserInfo.UserName}. Always address them by name.";

        return new ValueTask<AIContext>(new AIContext { Instructions = instructions });
    }

    public override ValueTask InvokedAsync(
        InvokedContext context,
        CancellationToken cancellationToken = default)
    {
        if (UserInfo.UserName is not null)
        {
            return default;
        }

        foreach (var message in context.RequestMessages)
        {
            if (message.Role != ChatRole.User)
            {
                continue;
            }

            var text = message.Text ?? "";
            const string Marker = "my name is";
            var index = text.IndexOf(Marker, StringComparison.OrdinalIgnoreCase);
            if (index < 0)
            {
                continue;
            }

            var rest = text[(index + Marker.Length)..].Trim();
            var name = rest.Split(' ', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
            if (!string.IsNullOrEmpty(name))
            {
                UserInfo.UserName = char.ToUpperInvariant(name[0]) + name[1..].ToLowerInvariant();
            }
        }

        return default;
    }
}

挂到 agent 上走 ChatClientAgentOptions.AIContextProviders

csharp 复制代码
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
    .AsAIAgent(new ChatClientAgentOptions
    {
        Name = "MemoryAgent",
        ChatOptions = new ChatOptions
        {
            Instructions = "You are a friendly assistant."
        },
        AIContextProviders = [new UserMemoryProvider()]
    });

AgentSession session = await agent.CreateSessionAsync();
Console.WriteLine(await agent.RunAsync("Hello! What's the square root of 9?", session));
Console.WriteLine(await agent.RunAsync("My name is Alice", session));
Console.WriteLine(await agent.RunAsync("What is 2 + 2?", session));

InvokingAsync 返回的 AIContext.Instructions 是追加,不是覆盖构造时的 instructions。模型实际看到的是原始说明书加上各 provider 注进去的段落。

三轮调用的时间顺序比类名重要。

第一轮 InvokingAsyncUserName 是空的,追加「你还不知道名字,礼貌地问」。用户问的是 9 的平方根。

第二轮用户说 My name is AliceInvokingAsync 仍然先跑,此时 InvokedAsync 还没写过名字,追加的仍是「去问名字」。调用结束后 InvokedAsyncRequestMessages,用很笨的字符串匹配找到 my name is,把 Alice 写进 UserInfo。这一轮模型未必会用 Alice 称呼人:名字在开口之后才入库。匹配本身也不理解「我是」「叫我」这类变体。框架没有提供自然语言抽取。教程里还有另一种写法:在 InvokedAsync 里另调一次聊天客户端,按 schema 抽出名字和年龄。上面这段只对应入门那种字符串钩子。

第三轮问 2+2。InvokingAsync 读到名字,追加「用户叫 Alice,请用名字称呼」。回答里带上名字,是因为你把事实钉回每次都会送进模型的那一层。

少传 session,provider 没有稳定的会话可挂。只传 session、不挂 context provider,模型仍可能靠历史接上「你还记得我什么」;一旦历史被截断或压缩,名字作为普通句子消失,说明书却不会自动补上。

多个 provider 可以同时挂在 AIContextProviders 里。历史那条线在 C# 上是 ChatHistoryProvider / ChatHistoryProviderFactory,例如内存历史加裁剪:

csharp 复制代码
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
    .AsAIAgent(new ChatClientAgentOptions
    {
        Name = "MemoryAgent",
        ChatOptions = new ChatOptions { Instructions = "You are a friendly assistant." },
        ChatHistoryProviderFactory = (ctx, ct) => new ValueTask<ChatHistoryProvider>(
            new InMemoryChatHistoryProvider(
                new MessageCountingChatReducer(2),
                ctx.SerializedState,
                ctx.JsonSerializerOptions,
                InMemoryChatHistoryProvider.ChatReducerTriggerEvent.AfterMessageAdded)),
        AIContextProviders = [new UserMemoryProvider()]
    });

向量检索那类历史增强用 ChatHistoryMemoryProvider,按 UserId / SessionId 控制存和搜的范围。Mem0 那套组合出现在 Python 入门里;C# 侧对应的是 AIContextProviders 列表加你选的 history provider,不要把 Python 的 before_run 参数名搬过来。

到这一层,单 agent 该有的四件事齐了:连接模型、调用函数、连续对话、把关键事实从历史里单独保存并写回说明书。再往下是分叉。步骤顺序已经确定,用 workflow。任务仍然开放、但会跨很多步,用 Harness。


框架的另一条轴:Workflow

Workflow 不是「更强的 agent」。它是框架里用来钉死顺序的那条轴。节点叫 Executor,边规定数据怎么走。模型在这张图里没有改道权。选用标准和 Overview 里那张表相同。

csharp 复制代码
using Microsoft.Agents.AI.Workflows;

Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");

sealed class ReverseTextExecutor() : Executor<string, string>("ReverseTextExecutor")
{
    public override ValueTask<string> HandleAsync(
        string message,
        IWorkflowContext context,
        CancellationToken cancellationToken = default)
        => ValueTask.FromResult(string.Concat(message.Reverse()));
}

var reverse = new ReverseTextExecutor();
var builder = new WorkflowBuilder(uppercase);
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
var workflow = builder.Build();

await using Run run = await InProcessExecution.RunAsync(workflow, "Hello, World!");
foreach (WorkflowEvent evt in run.NewEvents)
{
    if (evt is ExecutorCompletedEvent completed)
    {
        Console.WriteLine($"{completed.ExecutorId}: {completed.Data}");
    }
}

入门例子只做两步字符串处理:先变大写,再反转,用来看节点和边怎么接。BindAsExecutor 把函数登记成节点。AddEdge 连线,WithOutputFrom 指定从哪个节点取最终结果。少写后一句,图可能跑完了,你拿不到预期输出。InProcessExecution.RunAsync 在当前进程执行。进度看 ExecutorCompletedEvent:哪个节点完成、当时手里是什么数据。

Python 仓库里还有函数式工作流。C# 主路径是 Graph API。托管时整张图可以 .AddAsAIAgent(),对外仍是一个 agent。框架对内承认图和 agent 不同,对外希望协议只认一种对象。


框架给长任务的默认装配:Harness

Harness 仍是 agent,不是第三种运行时。概念页把它写成现有积木的组合:chat client、负责函数调用和压缩的 pipeline、todo / 模式 / 记忆等 context provider、批准和观测的 middleware。C# 类型是 HarnessAgent,由 IChatClient.AsHarnessAgent() 得到。

默认带上的东西,就是长任务里反复要手写的那些:规划与执行模式、todo、上下文压缩、会话级文件记忆、可选文件访问、「不要再问了」的批准。概念页默认开 OpenTelemetry,客户端支持则挂 hosted web search。窗口和输出上限在示例里出现过 128_00016_384

csharp 复制代码
AIAgent agent = chatClient.AsHarnessAgent();
AgentSession session = await agent.CreateSessionAsync();

while (true)
{
    Console.Write("> ");
    string? input = Console.ReadLine();
    if (string.IsNullOrWhiteSpace(input) ||
        input.Equals("exit", StringComparison.OrdinalIgnoreCase))
    {
        break;
    }

    await foreach (var update in agent.RunStreamingAsync(input, session))
    {
        Console.Write(update);
    }

    Console.WriteLine();
}

Harness 的状态(计划、待办、历史)放在 session 上,所以典型用法是循环加同一条 session,而不是单次 RunAsync。角色说明和操作说明是两层:

csharp 复制代码
AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
    Name = "research-agent",
    HarnessInstructions = "Use tools deliberately and report verified results.",
    ChatOptions = new ChatOptions
    {
        Instructions = "You are a research assistant focused on academic sources.",
    },
    MaxContextWindowTokens = 128_000,
    MaxOutputTokens = 16_384,
});

ChatOptions.Instructions 管它是谁。HarnessInstructions 管长任务怎么执行。可以关 DisableTodoProviderDisableToolAutoApprovalDisableCompactionDisableWebSearch,也可以加 FileAccessStore、后台子 agent、LoopEvaluators。Go 的 Harness 还是 coming soon。样例在 dotnet/samples/02-agents/Harness


框架怎样把 Agent 送出进程:Hosting

最后一层是托管。内部抽象不变,还是 AIAgentMicrosoft.Agents.AI.Hosting 从 DI 里取出这个对象,按协议翻译请求和响应。官方四条路:

A2A,给多 agent 互叫。

OpenAI 兼容端点,给已经会说 Chat Completions / Responses 的客户端。

Durable Extension,让 C# / Python 的 agent 和 workflow 在 Azure Functions 或自托管上可持久。

AG-UI,给网页。

csharp 复制代码
var pirateAgent = builder.AddAIAgent(
        "pirate",
        instructions: "You are a pirate. Speak like a pirate",
        description: "An agent that speaks like a pirate.",
        chatClientServiceKey: "chat-model")
    .WithAITool(new MyTool())
    .WithInMemorySessionStore();

登记名 "pirate" 是钥匙。chatClientServiceKey 指向已经注册过的 IChatClientWithAITool 把工具挂在这份登记上。WithInMemorySessionStore 把对话放在内存里------进程退出即消失,多实例不共享。文档写明生产要自己提供 AgentSessionStore

工作流目前没有自己的 A2A / OpenAI 集成,对外先 .AddAsAIAgent()。然后:

csharp 复制代码
builder.Services.AddA2AServer();
var app = builder.Build();
app.MapA2AServer();
app.Run();

一条请求从进门到出门

已经托管的话,入口可能是 A2A、OpenAI 兼容端点或 AG-UI。Hosting 把外部输入译成 RunAsync(...)。译完之后,和控制台里自己调用是同一条代码路径。

带了 session,就先取出这条会话上的消息历史,以及 provider 写在会话状态里的数据。没带,两样都空:模型看不见上一句,也看不见存过的名字。

然后执行各 AIContextProvider.InvokingAsync。名字已经在 UserInfo 里的话,这里会往 AIContext.Instructions 里追加「请用这个名字称呼」。Harness 若开着,todo、模式、文件记忆也在这里注入。模型还没调用。它即将读到的是:构造 agent 时的 instructions,加上刚刚追加的段落。

这之后才到模型。已登记的工具和 MCP 会作为函数说明送进去,模型选择回答或点名。需要批准的工具在执行前停住。中间件可以插在这条链上;Harness 默认把工具批准和 OpenTelemetry 放在这里。

模型返回后跑 InvokedAsyncUserMemoryProvider 从本轮 RequestMessages 里找 my name is。History provider 把本轮消息存进历史。Harness 会把计划和待办写回同一条 session,所以循环里下一次 RunAsync 改的是这份计划。

开了流式,AgentResponseUpdate 往外推,Hosting 再写成协议响应。不开就等 AgentResponse 一次性返回。返回值里不只有答案:同一份响应可以带 FunctionCallContentFunctionResultContent、推理文本。response.Text 只是把所有 TextContent 拼起来。

Workflow 不经过「模型选择下一步」。InProcessExecution.RunAsync 按边把结果交给下一个 Executor。进度看 ExecutorCompletedEvent。外部协议要喊到这张图,需要先 .AddAsAIAgent(),否则按 AIAgent 在容器里取会取空。

调用顺序可以写成:

协议 → RunAsync → 读 session → InvokingAsync → 模型(工具 / 中间件)→ InvokedAsync → 写回 session → 协议

session 没传,后面的 provider 和 Harness 状态都没有落点。


按缺口去装配

做一个能查天气、能记住名字、最后能被协议喊到的 C# agent,不必一次打开七层。哪一层缺了再补哪一层。

  • AIProjectClient.AsAIAgentRunAsync、能 RunStreamingAsync,连接就通了。
  • 天气是编的,再把函数交给 AIFunctionFactory.Create[Description] 写明白。
  • 第二句对不上第一句,同一条 AgentSession 传进 RunAsync
  • 对话一长名字就没了,用 AIContextProviderInvokingAsync 里把名字写回 AIContext.Instructions
  • 必须先核验再发信这种顺序,单独建 WorkflowBuilder,不要写进 instructions
  • 一单跨很多步、中途要改计划,用 AsHarnessAgent,循环里复用 session。
  • 只有本机能调,再 AddAIAgent。生产环境把 DefaultAzureCredential 和内存 session store 换掉。

C# 对应链:AsAIAgentAIFunctionFactory.CreateCreateSessionAsyncAIContextProvidersWorkflowBuilderAsHarnessAgentMicrosoft.Agents.AI.Hosting

C# 和 Python 可以从最小 agent 走到托管。Go 能做连接、工具、session、一部分 workflow 和 A2A;Harness、函数式工作流、声明式 agent、RAG、CodeAct 还没有。内容过滤和第三方模型合规不在入门范围里。框架提供批准、middleware、telemetry。MCP、CodeAct、后台子 agent 在 Concepts / Integrations,加进来之后通常还是一个 AIAgent,托管不用另起一套。

learn.microsoft.com/en-us/agent...

learn.microsoft.com/en-us/agent...

相关推荐
全栈弄潮儿11 分钟前
从零搭建你的 AI 编程工作流
aigc·openai·ai编程
Web3_Basketball16 分钟前
从0到1落地MCP连接器企业工具:踩坑全记录
ai编程
十一捉一23 分钟前
挑战在 Coding Agent 时彻底推翻之前的方案
agent·ai编程
大模型丫丫25 分钟前
如何高效部署开源AI编程助手:OpenCode实战指南
开源·ai编程
OriginCoding26 分钟前
用 AI Agent 协作完成一个 Android TOTP 应用:从需求边界到 v1.0.0
android·ai编程
zhangfeng11334 小时前
WebIDE 容器中 AtomCode 报 `ATOMCODE_SIG_STALE` 排查实录:一次由 8 小时时钟偏差引发的“悬案”
编辑器·ai编程·atomcode
星核0penstarry4 小时前
HICOOL 2026深度解读:从四大仪式看北京硬科技生态的底层逻辑
大数据·人工智能·科技·ai·创业创新·ai编程
wabil6 小时前
[AI-Talk] codx制作xmind文件技巧
ide·ai编程·myeclipse·xmind