Files
MAF1/MAF1.Route/HandoffGraphBuilder.cs
admin777 b0e89b6092 feat: 新增方案 C 的 Handoff 路由宿主,并让抽城市支持自然语言。
把图 JSON 编译成 CreateHandoffBuilderWith + WithHandoff;Agent 固定 Id 以免交接工具名错位;DeepSeek 关闭 thinking,避免多轮丢掉 reasoning_content 导致 400。
2026-09-01 17:56:41 +08:00

178 lines
7.6 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using MAF1.Agents;
using MAF1.Agents.FileCity;
using MAF1.Agents.Weather;
using MAF1.Tools;
using MAF1.Utils;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
namespace MAF1.Route;
/// <summary>
/// 把方案 C 的图编译成 MAF Handoff:节点 = 白名单(主管可第一跳交给其中任何一个);
/// 边 = 专家之间允许的交接。主管根据用户原话自己选第一跳,不必先经过拓扑入口。
/// </summary>
public static class HandoffGraphBuilder
{
public static Workflow Build(AgentFactory factory, WeatherTools weatherTools, RouteGraph graph, RouteUserInput input)
{
AIAgent router = factory.CreateAgent(
name: "router",
description: "路由主管:根据用户任务把对话交接给图上允许的专家,自己不读文件、不查天气。",
instructions: BuildRouterInstructions(graph, input));
Dictionary<string, AIAgent> specialists = new(StringComparer.OrdinalIgnoreCase);
foreach (RouteNode node in graph.Nodes)
{
specialists[node.Id] = CreateSpecialist(factory, weatherTools, node, graph, input);
}
HandoffWorkflowBuilder builder = AgentWorkflowBuilder.CreateHandoffBuilderWith(router);
foreach (RouteNode node in graph.Nodes)
{
builder.WithHandoff(router, specialists[node.Id], HandoffReason(node, fromRouter: true, when: null));
}
foreach (RouteEdge edge in graph.Edges)
{
builder.WithHandoff(
specialists[edge.From],
specialists[edge.To],
HandoffReason(graph.Find(edge.To)!, fromRouter: false, when: edge.When));
}
List<AIAgent> mustHandoff = [router];
foreach (RouteNode node in graph.Nodes)
{
if (graph.HasOutgoing(node.Id))
{
mustHandoff.Add(specialists[node.Id]);
}
}
return builder
.WithName("Route-Handoff")
.EmitAgentResponseUpdateEvents()
.EmitAgentResponseEvents()
.WithAutonomousMode(
turnLimit: 4,
continuationPrompt: "你还没有完成交接。请立即调用 handoff 工具把对话交给下一位专家,不要自己结束本轮。",
agents: mustHandoff)
.Build();
}
private static AIAgent CreateSpecialist(
AgentFactory factory,
WeatherTools weatherTools,
RouteNode node,
RouteGraph graph,
RouteUserInput input)
{
string title = string.IsNullOrWhiteSpace(node.Title) ? node.Type : node.Title;
string extra = BuildSpecialistExtra(node, graph, input);
return node.Type switch
{
SystemNodeTypes.FileCity => FileCityAgent.Create(
factory,
name: node.Id,
description: $"{title}{FileCityAgent.DefaultDescription}",
extraInstructions: extra),
SystemNodeTypes.Weather => WeatherAgent.Create(
factory,
weatherTools,
name: node.Id,
description: $"{title}{WeatherAgent.DefaultDescription}",
extraInstructions: extra),
_ => throw new InvalidOperationException($"未知节点类型 {node.Type}"),
};
}
private static string BuildRouterInstructions(RouteGraph graph, RouteUserInput input)
{
string entries = string.Join("、", graph.Nodes.Select(n => $"{n.Id}({n.Type})"));
string edges = graph.HasEdges
? string.Join("", graph.Edges.Select(e => $"{e.From}->{e.To}" + (string.IsNullOrWhiteSpace(e.When) ? "" : $"[{e.When}]")))
: "专家之间无边";
return
$"""
你是交接图上的主管。禁止自己读文件或查询天气。
白名单专家:{entries}
收到用户任务后,必须立刻 handoff 给最合适的一位,不要经过用不上的专家。
判断规则:
- 用户已经给出明确城市名、或只要查天气(如「拉斯维加斯」「帮我查成都天气」)→ 直接交给天气专家,不要先抽城市。
- 用户给的是文件路径、或一段需要从中识别城市的长文本 → 先交给抽城市专家。
专家之间的边:{edges}
不要向用户叙述交接过程,也不要在未交接时直接结束。
""";
}
private static string BuildSpecialistExtra(RouteNode node, RouteGraph graph, RouteUserInput input)
{
List<string> lines =
[
$"你是交接图上的节点 `{node.Id}`。",
"完成自己的工作后:若有下游专家,必须调用框架注入的 handoff 工具把对话交出去;不要只输出结果就结束。",
"若没有有效输入(例如抽不到城市),不要交接,用中文说明原因。",
"不要向用户叙述交接过程。",
];
if (node.Type == SystemNodeTypes.FileCity)
{
lines.Add(
"""
用户输入可能是文件路径,也可能是描述/问句/城市名单。你自己判断:
- 像本地路径且需要读内容 调用 ReadTextFile
- 像「帮我查成都天气」「明天去上海」→ 不要读文件,从用户消息提取城市。
不要把自然语言当成文件路径。
""");
}
List<RouteEdge> outgoing = graph.Edges
.Where(e => e.From.Equals(node.Id, StringComparison.OrdinalIgnoreCase))
.ToList();
if (outgoing.Count == 0)
{
lines.Add("你没有下游专家。做完后直接回答用户,不要再交接。");
}
else
{
lines.Add("下游:" + string.Join("", outgoing.Select(e =>
{
RouteNode to = graph.Find(e.To)!;
string when = string.IsNullOrWhiteSpace(e.When) ? "" : $",条件 {e.When}";
return $"{to.Id}{to.Type}{when}";
})));
}
return string.Join("\n", lines);
}
private static string HandoffReason(RouteNode target, bool fromRouter, string? when)
{
string title = string.IsNullOrWhiteSpace(target.Title) ? target.Type : target.Title;
string role = target.Type switch
{
SystemNodeTypes.FileCity => FileCityAgent.DefaultDescription,
SystemNodeTypes.Weather => WeatherAgent.DefaultDescription,
_ => target.Type,
};
string prefix = fromRouter
? target.Type switch
{
SystemNodeTypes.FileCity => "用户给了文件或需要从文本里识别城市时,交给这位专家。已经是明确城市名、只要查天气时不要交给他。",
SystemNodeTypes.Weather => "用户已经给出城市名或明确要查天气时,直接交给这位专家,不必先抽城市。",
_ => "把用户任务交给这位专家。",
}
: "把对话交给下一位专家。";
string condition = string.IsNullOrWhiteSpace(when)
? ""
: when.Equals("hasValidCities", StringComparison.OrdinalIgnoreCase)
? " 仅当已经抽出有效城市时才交接。"
: when.Equals("!hasValidCities", StringComparison.OrdinalIgnoreCase)
? " 仅当没有有效城市时才交接。"
: $" 条件:{when}。";
return $"{prefix} 节点 {target.Id}{title}):{role}{condition}";
}
}