feat: 新增方案 C 的 Handoff 路由宿主,并让抽城市支持自然语言。
把图 JSON 编译成 CreateHandoffBuilderWith + WithHandoff;Agent 固定 Id 以免交接工具名错位;DeepSeek 关闭 thinking,避免多轮丢掉 reasoning_content 导致 400。
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
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}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user