Ohhnews

分类导航

$ cd ..
foojay原文

BoxLang AI 深度解析:多智能体编排与协作架构

#boxlang#人工智能#多智能体系统#软件架构#自动化编排

BoxLang AI 深度解析 — 第 3 部分(共 7 部分):多智能体编排 — 构建高效的 AI 团队 🌲

BoxLang AI 3.0 系列 · 第 3 部分(共 7 部分)

单一智能体固然有用,但智能体集群才真正强大。

大多数多智能体框架的问题在于编排层是“外挂”上去的——你需要手动管理智能体引用,手动在它们之间传递输出,还得祈祷自己没有引入循环引用。它们通常缺乏层级概念、循环检测机制,也无法回答“谁负责这里?”或“我处于树状结构的哪一层?”这类问题。

BoxLang AI 3.0 改变了这一点。AiAgent 现在可以跟踪其在完整层级树中的位置,并且子智能体会自动被配置为工具——协调者无需知道如何进行委派,只需知道它有权委派即可。

🌲 智能体树(The Agent Tree)

每个 AiAgent 都拥有一个 parentAgent 属性和一套完整的层级辅助方法。这种关系是双向的:addSubAgent() 在一次调用中即可将子智能体注册为可调用的工具,并设置父级引用。

coordinator = aiAgent( name: "coordinator" )
    .addSubAgent( aiAgent( name: "researcher" ) )
    .addSubAgent( aiAgent( name: "writer" ) )

// 层级查询
println( coordinator.isRootAgent() )           // true
println( coordinator.getAgentDepth() )         // 0

println( researcherAgent.isRootAgent() )       // false
println( researcherAgent.getAgentDepth() )     // 1
println( researcherAgent.getAgentPath() )      // /coordinator/researcher
println( researcherAgent.getAncestors() )      // [ coordinator ]

println( writerAgent.getRootAgent().getAgentName() ) // coordinator

源码中完整的层级 API 如下:

// 来自 AiAgent.bx
setParentAgent( parent )   // 分配父级 — 包含自引用和循环守卫
clearParentAgent()         // 从父级分离
hasParentAgent()           // 布尔值
isRootAgent()              // 当深度为 0 时返回 true
getRootAgent()             // 向上遍历,返回根节点
getAgentDepth()            // 0 = 根节点, 1 = 子节点, 2 = 孙节点, ...
getAgentPath()             // "/coordinator/researcher"
getAncestors()             // [直接父级, 祖父级, ..., 根节点]

内置循环检测

设置会导致循环的父级会立即抛出异常——绝不会出现静默的无限循环:

// 来自 AiAgent.bx — setParentAgent()
AiAgent function setParentAgent( required AiAgent parent ) {
    if ( arguments.parent == this ) {
        throw( type: "AiAgent.InvalidParent", message: "智能体不能是它自己的父级。" )
    }
    // 向上遍历拟定父级的祖先并检查是否存在自身
    var ancestor = arguments.parent
    while ( ancestor.hasParentAgent() ) {
        ancestor = ancestor.getParentAgent()
        if ( ancestor == this ) {
            throw( type: "AiAgent.CyclicParent", message: "设置此父级将导致循环..." )
        }
    }
    variables.parentAgent = arguments.parent
    return this
}

🤖 子智能体即工具

addSubAgent() 的魔力在于,每个子智能体都会自动封装为一个父级可以调用的工具——无需手动接线,无需编写自定义回调代码。

// 来自 AiAgent.bx — createSubAgentTool()
private ITool function createSubAgentTool( required AiAgent subAgent ) {
    var agentName    = arguments.subAgent.getConfig().name
    var toolName     = "delegate_to_" & agentName.slugify()
    var toolDescription = "将任务委派给 '#agentName#' 子智能体。"
        & "当任务符合子智能体的专业领域时使用此工具:#agentConfig.description#"

    return aiTool(
        name: toolName,
        description: toolDescription,
        callable: ( required task ) => {
            return subAgent.run( task )
        }
    ).describeArg( "task", "委派给子智能体的任务或问题" )
}

当调用 addSubAgent() 时,父级的 AiModel 会获得一个名为 delegate_to_researcherdelegate_to_writer 等的新工具。大模型(LLM)会在其上下文中看到这些工具,并像决定调用任何其他工具一样,决定何时使用它们。

协调者不需要特殊的逻辑,它只是拥有了更多的工具。

🏢 AiAgent 现在完全无状态

这是 3.0 版本最重要的架构变化之一:AiAgent 不再将 userIdconversationId 作为实例状态保存。它们是在每次调用时解析的。

// 来自 AiAgent.bx — run()
public any function run( any input = "", struct params = {}, struct options = {} ) {
    // 每次调用时解析 — 无共享状态
    var threadId       = arguments.options.threadId ?: variables.options.threadId ?: createUUID()
    var userId         = arguments.options.userId ?: variables.options.userId ?: ""
    var conversationId = arguments.options.conversationId ?: variables.options.conversationId ?: ""
    // ...
}

这意味着一个智能体实例可以安全地服务于多个并发用户——没有竞态条件,没有跨用户污染,也不需要按用户创建智能体工厂。

// 一个共享智能体 — 多个并发用户
sharedAgent = aiAgent( name: "support", memory: aiMemory( "cache" ) )

// 每次调用都是完全隔离的
sharedAgent.run( "Hello",           {}, { userId: "alice", conversationId: "sess-1" } )
sharedAgent.run( "What did I say?", {}, { userId: "alice", conversationId: "sess-1" } ) // 记得 alice
sharedAgent.run( "Hello",           {}, { userId: "bob",   conversationId: "sess-2" } ) // 与 alice 隔离

🧠 基于内存的按次身份路由

所有内存类型都遵循相同的模式——add()getAll()clear()trim() 都接受可选的 userIdconversationId

// 一个内存实例,多个租户
sharedMemory = aiMemory( "cache" )

sharedMemory.add( message, userId: "alice", conversationId: "conv-1" )
sharedMemory.add( message, userId: "bob",   conversationId: "conv-2" )

// 每次检索都是租户隔离的
aliceHistory = sharedMemory.getAll( userId: "alice", conversationId: "conv-1" )
bobHistory   = sharedMemory.getAll( userId: "bob",   conversationId: "conv-2" )

当智能体在内部调用 loadMemoryMessages() 时,它会将解析后的 userIdconversationId 传递给所有关联的内存。内存天然实现了租户隔离,无需额外配置。

🏗️ 智能体运行生命周期

了解 run() 内部发生的事情对于调试或构建中间件非常有用(第 4 部分将详细介绍)。以下是执行序列:

  1. 解析 threadId / userId / conversationId(每次调用解析,而非实例状态)
  2. 构建用户消息结构
  3. 构建系统消息(描述 + 指令 + 技能 + 工具 + MCP 服务器)
  4. 加载该 userId/conversationId 的内存消息
  5. 组装:[系统, ...内存, 用户消息]
  6. 触发 beforeAgentRun 中间件(前向传递)
  7. 触发 BoxAnnounce "beforeAIAgentRun"(全局事件)
  8. 通过 AiModel.run() 执行 — 处理工具调用、重试、流式传输
  9. 如果挂起 (HITL):检查点保存并返回
  10. 将助手响应存储在所有内存中(按 userId/conversationId 作用域)
  11. 触发 afterAgentRun 中间件(反向传递)
  12. 触发 BoxAnnounce "afterAIAgentRun"(全局事件)
  13. 返回响应

系统消息也会被缓存并生成指纹——如果自上次调用以来描述、指令和技能池没有变化,则使用缓存版本,而不是重新构建。这对于同一智能体处理大量请求的高吞吐场景非常重要。

🌊 多智能体团队的流式传输

流式传输在多智能体设置中工作方式相同——每个智能体都可以独立进行流式传输:

coordinator = aiAgent( name: "coordinator" )
    .addSubAgent( aiAgent( name: "researcher" ) )
    .addSubAgent( aiAgent( name: "writer" ) )

// 流式输出协调者的响应
coordinator.stream(
    onChunk : chunk => writeOutput( chunk.choices?.first()?.delta?.content ?: "" ),
    input   : "研究并撰写一篇关于 BoxLang JVM 互操作性的 500 字文章",
    options : { userId: "user-123", conversationId: "session-abc" }
)

当协调者决定委派给研究员时,该子调用会在工具调用内部同步发生——流式传输的协调者会以工具响应的形式收到研究员的结果,然后继续流式传输。

🔄 挂起与恢复

HumanInTheLoopMiddleware(第 4 部分涵盖)挂起智能体时,状态需要被保留。checkpointer 属性负责处理此逻辑:

agent = aiAgent(
    name        : "finance-bot",
    middleware  : new HumanInTheLoopMiddleware(
        mode                  : "web",
        toolsRequiringApproval: [ "transferFunds" ]
    ),
    checkpointer: aiMemory( "cache" )
)

// 第一次调用 — 挂起等待批准
result = agent.run( "转账 $500 到账户 #12345" )
// result.isSuspended() == true
// 智能体使用 threadId 自动保存检查点

// 稍后,在人类通过 Web UI 批准后
threadId = result.getData().threadId
agent.resume( "approve", threadId )

// 或者拒绝:
agent.resume( "reject", threadId )

// 或者编辑参数:
agent.resume( "edit", threadId, { correctedArgs: { amount: 100, account: "#12345" } } )

resume() 的实现会从保存的检查点重新运行,将人类的决策注入到中间件上下文中:

// 来自 AiAgent.bx — resume()
any function resume( required string decision, required string threadId, struct editedData = {} ) {
    var savedState = variables.checkpointer.loadState( arguments.threadId )
    // 清除状态,防止被重复恢复
    variables.checkpointer.clearState( arguments.threadId )
    // 重新运行,并将恢复上下文注入到选项中
    return run(
        savedState.input,
        savedState.params,
        savedState.options.append( { _resumeContext: {
            resumeDecision : arguments.decision,
            editedData     : arguments.editedData,
            suspendData    : savedState.suspendData
        } } )
    )
}

🔍 内省(Introspection)

getConfig() 方法让你能够全面洞察智能体的状态——对于调试、监控仪表板和记录日志非常有用:

config = agent.getConfig()

// 层级
println( config.agentDepth )    // 0, 1, 2, ...
println( config.agentPath )     // "/coordinator/researcher"
println( config.parentAgent )   // "coordinator" (名称字符串)

// 能力
println( config.toolCount )     // 总工具数(包括子智能体委派工具)
println( config.tools )         // [{ name, description }]
println( config.mcpServers )    // [{ url, toolNames }]

// 内存
println( config.memoryCount )
println( config.memories )

// 技能
println( config.activeSkillCount )
println( config.availableSkillCount )
println( config.skills )        // { activeSkills: [...], availableSkills: [...] }

// 中间件
println( config.middlewareCount )
println( config.middleware )    // [{ name, description }]

🚀 完整的多智能体示例

这是一个实际的编排示例:一个协调者将研究任务委派给专门的研究员,将写作任务委派给专门的写作者,两者都有各自的技能和工具。

// 专业智能体
researchAgent = aiAgent(
    name        : "researcher",
    description : "擅长从多个来源查找、分析和总结信息",
    instructions: "始终引用来源。优先考虑最新信息。清晰总结调查结果。",
    tools       : [ "searchWeb@tools", "fetchURL@tools" ],
    skills      : [ aiSkill( ".ai/skills/research-methodology/SKILL.md" ) ]
)

writerAgent = aiAgent(
    name        : "writer",
    description : "擅长将研究成果转化为精炼、引人入胜的文稿",
    instructions: "为技术受众撰写。清晰直接。拒绝废话。",
    skills      : [ aiSkill( ".ai/skills/writing-style/SKILL.md" ) ]
)

// 协调者,将上述两者作为子智能体
coordinator = aiAgent(
    name        : "content-coordinator",
    description : "编排研究和写作,以生成完整的文章",
    instructions: "将复杂请求分解为研究和写作阶段。相应地进行委派。",
    subAgents   : [ researchAgent, writerAgent ],
    memory      : aiMemory( "cache" )
)

// 检查层级
println( coordinator.isRootAgent() )               // true
println( researchAgent.getAgentPath() )            // /content-coordinator/researcher
println( coordinator.getConfig().toolCount )       // 2 (delegate_to_researcher + delegate_to_writer)

// 运行 — 协调者决定何时委派以及委派给谁
response = coordinator.run(
    "撰写一篇关于 BoxLang AI 功能的综合文章",
    {},
    { userId: "luis", conversationId: "article-session-1" }
)

驱动协调者的大模型会看到两个工具:delegate_to_researcherdelegate_to_writer。它决定先调用研究员,获得详细摘要,然后带着该摘要和原始请求调用写作者,最后将写作者的输出合成为最终响应。你无需编写任何逻辑——大模型根据工具描述自行完成了这一切。

接下来是什么

在第 4 部分中,我们将探讨中间件系统——六个内置的中间件类、钩子生命周期如何工作、如何编写自己的中间件,以及让 AI 智能体在 CI 中具备可测试性的 FlightRecorderMiddleware

📖 完整文档 📦立即安装:install-bx-module bx-ai 🫶专业支持