Skip to content

第 5 章(下):产品中的自定义 Agent 与 Tool

我的产品需要解决什么问题

前两节讲了 Tool 怎么与 LLM 交互、Agent 是什么。这一节讲我的产品里,自定义 Agent 和自定义 Tool 是怎么设计的。

我的产品(Growth AIOS)有 100+ 个工作流节点。用户可以用自然语言描述需求,Agent 自动生成工作流。

这个"自动生成"不是一步完成的,而是分成了多个步骤——需求解析、控制骨架设计、节点选型、参数填充、Canvas 构建、注册。

每个步骤需要的知识、工具、判断逻辑完全不同。一个 Agent 做不完。

自定义 SubAgent:不同类型的 Agent 做不同的事

不同场景需要不同的 Agent。但怎么管理这些 Agent?

csharp
// SubAgent 提供者接口
public interface IAgentSubAgentProvider
{
    // 获取所有已注册的 SubAgent 配置
    IReadOnlyList<SubAgentConfig> GetRegisteredAgents();
    
    // 执行指定的 SubAgent
    Task<SubAgentResult> ExecuteAsync(
        SubAgentConfig config, string prompt, CancellationToken cancellationToken = default);
}

每个 SubAgent 有自己的配置:

  • AgentType:类型标识(如 "WorkflowPlanner"、"CodeRunner")
  • SystemPrompt:这个 Agent 的系统提示词
  • ModelId:使用哪个模型(不同任务可以用不同模型)
  • Tools:这个 Agent 可用的工具集

同样是 Provider 可覆盖模式——核心层注册默认空实现,业务层按需注册。

Lead-Sub Agent 分层架构:LeadAgent 编排,SubAgent 执行

自定义 Lead-Sub Agent 分层:让 Agent 能“编排”

有了多个 SubAgent,需要一个"指挥官"来编排它们。

这就是 LeadAgent 的角色:

用户消息

LeadAgent(理解意图 + 拆分任务)
  ├── SubAgent A(执行子任务 1)
  ├── SubAgent B(执行子任务 2)
  └── SubAgent C(执行子任务 3)

LeadAgent 汇总结果 → 返回给用户

LeadAgent 的核心工具是 TaskTool——它让 LeadAgent 可以把任务"派发"给 SubAgent。

csharp
// LeadAgent 的工具构建
private IList<AITool> BuildLeadAgentTools(string sessionId)
{
    // 业务工具(由 Integration 层提供)
    var domainTools = _toolProvider.GetToolsForLeadAgent()
        .Select(t => WrapTool(t.ToAITool(), sessionId)).ToList();
    
    // TaskTool 是唯一内置工具(用于派发 SubAgent)
    domainTools.Add(WrapTool(_taskTool.ToAITool(), sessionId));
    
    return domainTools;
}

注意 TaskTool唯一内置的工具——其他工具都由业务层提供。这说明"派发任务给 SubAgent"是 LeadAgent 的核心能力,不是业务功能。

真实案例:用 Lead Agent + Sub Agent 创建工作流

这是产品设计思路最集中的体现。

用户说:"帮我创建一个工作流,读取图片并裁剪。"

这不是一步完成的。LeadAgent 把任务拆成 3 个阶段,每个阶段派发给不同的 SubAgent:

用户需求:"读取图片并裁剪"

LeadAgent 理解意图 → 派发给 SubAgent 1

SubAgent 1: 需求解析 Agent
  ├── System Prompt: "你是一个需求分析专家,从用户描述中提取操作对象、动作、流程特征"
  ├── Tool: 无(纯 LLM 理解)
  ├── 输出: 结构化需求(操作对象=图片, 动作=读取+裁剪, 流程=串行)
  └── 用户确认: ✅ / 💬调整 / ⬆回退

SubAgent 2: 控制骨架 Agent
  ├── System Prompt: "你是一个流程架构师,决定需要哪些控制节点(循环/条件/批处理)"
  ├── Tool: 无(控制节点是固定的)
  ├── 输出: 控制骨架(串行:读取→裁剪,无循环无条件)
  └── 用户确认: ✅ / 💬调整 / ⬆回退

SubAgent 3: 节点选型 Agent
  ├── System Prompt: "你是一个节点选型专家,从 100+ 节点中选出最合适的"
  ├── Tool: search_nodes(搜索节点), get_node_detail(查看节点详情)
  ├── 输出: 节点选型方案(读取=FileReadNode, 裁剪=ImageCropNode)
  └── 用户确认: ✅ / 💬调整 / ⬆回退

代码执行阶段(零 AI,纯代码自动执行)
  ├── Step 2: PlanEnricher → 查 form.json → 填默认值 → 建引用链
  ├── Step 3: CanvasBuilder → 生成 Canvas JSON → 结构校验
  └── Step 4: Register → 注册到工作流引擎 → 返回 DefinitionId

注意三个 SubAgent 的设计差异:

SubAgent需要什么知识需要什么工具用什么模型
需求解析理解自然语言无(纯理解)强理解力模型
控制骨架知道有哪些控制节点无(控制节点固定)中等模型
节点选型知道 100+ 节点的功能search_nodes + get_node_detail需要工具调用能力

每个 SubAgent 是一个独立的角色封装——自己的知识、自己的工具、自己的模型。LeadAgent 负责编排它们,但不做具体工作。

对话式迭代:不是线性流水线

这个流程不是"跑完就结束"的线性流水线。每一步都支持用户反馈:

SubAgent 输出结果 → 展示给用户
  ├── 用户说"合理" → 保存结果,前进到下一步
  ├── 用户说"调整:不是裁剪,是压缩" → SubAgent 吸收反馈,重新生成
  └── 用户说"需求理解错了,回到上一步" → 回退,级联清除下游数据

每步可以循环多轮,直到用户满意。这就是 Agentic Conversation Loop——AI 逐步生成、用户逐轮反馈、持续迭代直至满意。

关键设计:LLM 只做决策,代码做转换

注意 AI 阶段(3 个 SubAgent)和代码阶段(3 步)的分工:

  • LLM 做:语义理解、拓扑设计、节点选型(需要判断力的决策)
  • LLM 不做:拼 JSON、填默认值、建引用链、生成 Canvas(确定性转换)
  • 代码做:查 form.json → 填充默认值 → 匹配参数 → 建立数据流引用 → 生成 Canvas JSON

这个分工很重要——LLM 擅长决策,不擅长精确转换。 让 LLM 拼 JSON 一定会出错,但让代码做确定性转换永远不会出错。

LeadAgent 的完整调用链

LeadAgent 的一次调用,涉及整个 Agent 系统的协作:

csharp
public async Task<ChatResponse> ChatAsync(ChatRequest request, CancellationToken cancellationToken)
{
    // 1. 确保会话存在
    var (sessionId, _) = await EnsureSessionAndTraceAsync(request);
    
    // 2. 解析模型
    var modelInstanceId = ResolveModelInstanceId(request);
    
    // 3. 构建系统指令(含 SubAgent 描述)
    var instructions = _leadAgentPrompt.BuildInstructions(_subAgentProvider);
    
    // 4. 构建工具(业务工具 + TaskTool)
    var tools = BuildLeadAgentTools(sessionId);
    
    // 5. 通过工厂创建 Agent(内部自动挂载所有 Provider + 装饰器)
    var agent = await _agentFactory.CreateLeadAgentAsync(
        sessionId, modelInstanceId, instructions, tools, cancellationToken);
    
    // 6. 创建 Session + 设置 StateBag
    var agentSession = await agent.CreateSessionAsync(sessionId, cancellationToken);
    agentSession.StateBag.SetValue(MemoryFactsContextProvider.SessionIdKey, 
        new SessionIdWrapper { Value = sessionId });
    
    // 7. 执行(框架自动处理完整生命周期)
    var agentResponse = await agent.RunAsync(
        request.Message, session: agentSession, cancellationToken: cancellationToken);
    
    // 8. 持久化 + 标题生成
    StoreMessage(sessionId, "user", request.Message);
    StoreMessage(sessionId, "assistant", agentResponse.Text ?? "");
    EnsureTitle(sessionId, request.Message);
    
    return new ChatResponse { SessionId = sessionId, Content = agentResponse.Text, IsSuccess = true };
}

关键在第 5 步和第 7 步:

  • 第 5 步AgentFactory 内部自动挂载所有 AIContextProvider(记忆、监控、标题、Skill)和装饰器管道(重试、日志、压缩)
  • 第 7 步agent.RunAsync() 自动执行完整生命周期——加载上下文 → 调用 LLM → 工具调用循环 → 返回结果

LeadAgentManager 本身是一个薄编排层——它不做具体工作,只负责把各个组件串起来。

工具的可观测性包装

每个工具调用都被包装了一层"可观测性":

csharp
private AITool WrapTool(AITool tool, string sessionId)
{
    if (_agentEventBus != null && _eventBuffer != null && tool is AIFunction aiFunc)
    {
        return new ObservingAIFunction(aiFunc, _agentEventBus, _eventBuffer, sessionId);
    }
    return tool;
}

ObservingAIFunction 在工具调用前后发布事件:

  • ToolCalledEvent:工具开始执行
  • ToolCompletedEvent:工具执行完成(含耗时、结果)

这让监控系统能追踪每个工具的使用情况——哪个工具用得最多、哪个工具最慢、哪个工具经常失败。

SubAgent 的执行隔离

每个 SubAgent 有自己独立的上下文:

  • 自己的系统提示词
  • 自己的工具集
  • 自己的模型选择
  • 自己的执行环境

SubAgent 之间不共享状态——它们通过 LeadAgent 间接通信。

LeadAgent
  ├── SubAgent A(独立上下文)
  ├── SubAgent B(独立上下文)
  └── SubAgent C(独立上下文)

这种设计的好处是隔离性——一个 SubAgent 出问题不会影响其他 SubAgent。坏处是开销——每个 SubAgent 都要独立调用 LLM。

业界怎么做的

同样的"自定义 Agent + 多 Agent 编排"问题,其他框架是怎么解决的?

自定义 Tool 的注册方式

LangGraph 的做法是"工具绑定到节点"——每个图节点可以有自己的工具集,Agent 在某个节点上只能看到该节点绑定的工具。好处是工具权限控制精确,坏处是工具不能跨节点共享。

微软 Agent Framework (MAF) 的做法是通过 ChatOptions.Tools 传入工具——工具在每次调用时注入,不是在 Agent 构造时写死。这意味着同一个 Agent 在不同场景下可以看到不同的工具集。MAF 的 FunctionInvokingChatClient 自动处理工具调用循环。

Hermes 的做法是"工具自动发现"——新增一个工具,Hermes 自动让所有 Agent 知道它的存在。不需要手动注册。这比我的"Provider 注册"更自动化,但灵活性差一些——你无法控制"哪个 Agent 看到哪些工具"。

多 Agent 编排方式

LangGraph 的多 Agent 通过"子图"实现——一个节点可以是一个完整的子图,子图内部有自己的 Agent 和工具。好处是可视化、可控;坏处是灵活性受限于图结构,加一个新 Agent 就要改图。

AgentScope Java 的做法是"Pipeline + 消息传递"——Agent 之间通过消息管道通信。每个 Agent 是一个"处理器",接收消息、处理、输出。多 Agent 编排通过 Pipeline 组合实现(串行、并行、条件分支)。好处是解耦彻底;坏处是调试困难——消息在管道里流转,出错了不好定位。

我的设计是 Lead-Sub 分层——LeadAgent 通过 TaskTool 派发任务给 SubAgent,SubAgent 执行完返回结果。LeadAgent 是唯一有“派发权”的 Agent,SubAgent 之间不直接通信。

对比总结

框架Tool 注册多 Agent 编排特点
LangGraph节点绑定工具子图可视化、可控,但灵活性差
MAFChatOptions.Tools 动态注入Agent 组合 + 装饰器管道最接近我的设计
AgentScope处理器绑定Pipeline + 消息传递解耦彻底,调试困难
Hermes全局自动发现无内置多 Agent自动化高,灵活性差
我的设计Provider 可覆盖Lead-Sub 分层 + TaskTool业务层按需注册,Agent 看不同工具集

关键认知

这个阶段做完,我有了第五个关键认知:

自定义 Agent 不是"重新写一个 Agent",是"在基座上组合能力"。

  • Tool 是 Agent 的"手"——能操作外部世界
  • SubAgent 是 Agent 的"分身"——每个分身有自己的专长
  • LeadAgent 是 Agent 的"大脑"——理解意图、拆分任务、编排执行

三者组合在一起,就是 Agent 的"能力体系":

LeadAgent(大脑)
  ├── Tool(手)→ 操作文件、执行命令、调用 API
  ├── SubAgent(分身)→ 专业领域执行
  └── TaskTool(调度)→ 把任务分配给分身

但到这里,所有组件还是散的。记忆、检索、Skill、Rule、Tool、Agent——它们各自独立工作,缺少一个"全景图"把它们串起来。

这就是下一章要解决的问题:Harness 架构全景。