12 - Agent专题与设计模式

零配置注册、插件架构、Pipeline流水线、MCP工具协议

一、零配置Agent注册

新增Agent只需要两步:

零配置Agent注册:

  Step 1: 实现Agent接口 + @Component
  ┌─────────────────────────────────────────────┐
  │ @Component                                  │
  │ public class MyNewAgent extends AbstractLlmAgent { │
  │                                             │
  │   @Override public String getId() { return "my-agent"; } │
  │   @Override public String getName() { return "我的Agent"; } │
  │   @Override public String getDescription() { ... } │
  │   @Override public String getSystemPrompt() { ... } │
  │   @Override                                 │
  │   public Set<AgentCapability> getCapabilities() { │
  │     return Set.of(MODEL_SELECTION, PLAN_MODE); │
  │   }                                         │
  │ }                                           │
  └─────────────────────────────────────────────┘

  Step 2: 重启应用 (或通过Plugin API动态注册)
  ┌─────────────────────────────────────────────┐
  │ AgentRegistry 自动发现:                     │
  │                                             │
  │ @Component                                  │
  │ AgentRegistry(List<Agent> agents) {         │
  │   // Spring自动注入所有Agent实现            │
  │   agents.forEach(a -> map.put(a.getId(), a));│
  │ }                                           │
  │                                             │
  │ ✅ 新Agent自动出现在前端Agent列表           │
  │ ✅ 自动获得SSE流式响应                      │
  │ ✅ 自动获得安全防护                          │
  │ ✅ 自动获得能力驱动的UI组件                 │
  └─────────────────────────────────────────────┘

二、插件系统

支持运行时动态注册/卸载Agent:

插件系统架构:

  PluginManager
    │
    ├── loadPlugin(jarPath/markdownPath)
    │   ├── 从JAR或Markdown文件加载Agent定义
    │   ├── PluginAgent 包装为标准Agent接口
    │   └── AgentRegistry.registerAgent(pluginAgent)
    │
    ├── unloadPlugin(agentId)
    │   ├── AgentRegistry.unregisterAgent(agentId)
    │   └── 清理资源
    │
    └── PluginController (REST API)
        ├── POST /plugins/load    加载插件
        ├── DELETE /plugins/{id}  卸载插件
        └── GET /plugins          列出已加载插件

  MarkdownPromptAgent:
  ┌──────────────────────────────────┐
  │ # 翻译助手                       │
  │                                  │
  │ ## 系统提示词                    │
  │ 你是一个专业的翻译助手...        │
  │                                  │
  │ ## 配置                          │
  │ - id: translate                  │
  │ - capabilities: MODEL_SELECTION  │
  └──────────────────────────────────┘
  → 自动解析为可运行的Agent

三、Pipeline流水线

多阶段Agent处理流水线,支持PROCESSOR(处理器)和REVIEWER(审核者):

AgentPipelineExecutor:

  Pipeline: [Processor1 → Processor2 → Reviewer1 → Reviewer2]

  输入: "帮我优化这份简历"
       │
       ▼
  ┌──────────────────────────────────────────┐
  │ Processor Stage 1: 简历解析              │
  │   输出: 结构化简历数据                   │
  │   → 累积输出传递给下一阶段              │
  ├──────────────────────────────────────────┤
  │ Processor Stage 2: 问题分析              │
  │   输入: [前序输出] + 原始请求            │
  │   输出: 优化建议列表                     │
  ├──────────────────────────────────────────┤
  │ Reviewer Stage 1: 质量审核               │
  │   解析: SCORE: 85, REASONING: ...        │
  │   通过阈值: 60分                         │
  ├──────────────────────────────────────────┤
  │ Reviewer Stage 2: 合规审核               │
  │   解析: SCORE: 92                        │
  └──────────────────────────────────────────┘
       │
       ▼
  最终结果: min(85, 92) = 85分, 全部输出, 问题列表

四、Hook系统 — 生命周期钩子

Hook生命周期:

  BEFORE_AGENT_EXECUTION  ← 执行前 (可阻止执行)
       │
  ON_SYSTEM_PROMPT        ← 系统提示词构建后 (可修改提示词)
       │
  BEFORE_LLM_CALL         ← LLM调用前 (可修改消息)
       │
      [LLM调用]
       │
  AFTER_LLM_CALL          ← LLM调用后 (可处理响应)
       │
  AFTER_AGENT_EXECUTION   ← Agent执行完成后 (异步,不阻塞)

  Hook接口:
  ┌──────────────────────────────────────┐
  │ public interface AgentHook {         │
  │   Set<HookType> supportedTypes();   │
  │   int getOrder();     // 执行顺序    │
  │   boolean isAsync();  // 是否异步    │
  │   void execute(HookContext ctx);     │
  │ }                                    │
  └──────────────────────────────────────┘

  应用场景:
  · 日志记录Hook: AFTER_LLM_CALL记录每次调用
  · 限流Hook: BEFORE_AGENT_EXECUTION检查配额
  · 内容审核Hook: AFTER_LLM_CALL审核输出
  · 统计Hook: AFTER_AGENT_EXECUTION更新指标

五、MCP工具协议

MCP(Model Context Protocol)是标准化的工具调用协议:

MCP Client架构:

  AbstractLlmAgent
       │ 工具调用
       ▼
  McpClientManager
       │
       ├── 工具注册表:
       │   logicalName → McpServer (首个注册优先)
       │   "server.tool" → McpServer (限定名)
       │
       ├── 连接管理:
       │   · connect(server) → 建立连接
       │   · disconnect(serverId) → 断开连接
       │   · 自动重连 (60s间隔)
       │
       └── 工具执行:
           · tools/list → 发现可用工具
           · tools/call → 执行工具
           · 支持SSE流式工具结果

  传输方式:
  ┌──────────┬──────────┬──────────┐
  │ SSE      │ HTTP     │ Stdio    │
  │ (常用)   │ (备选)   │ (本地)   │
  └──────────┴──────────┴──────────┘

六、工具路由前缀

前缀路由目标示例
skill_SkillToolProviderskill_search → 内置搜索技能
shell_ShellToolProvidershell_ls → 执行Shell命令
nos_NosUploadToolProvidernos_upload → 上传到对象存储
其他McpClientManagerweb_search → MCP工具

七、HITL — 人机协同(Human-in-the-loop)

工具审批流程 (HITL):

  LLM决定调用工具: shell_rm("/tmp/old_data")
       │
       ▼
  shouldPauseForApproval() → true (高风险工具)
       │
       ▼
  创建审批工单 → ToolApprovalService
       │
       ├── 发送 SSE事件: tool_approval_required
       │   {toolName, arguments, approvalId}
       │
       ▼
  前端显示审批对话框:
  ┌─────────────────────────────────┐
  │ ⚠️ 工具执行审批                │
  │                                 │
  │ 工具: shell_rm                  │
  │ 参数: /tmp/old_data             │
  │                                 │
  │ [✅ 批准]  [❌ 拒绝]            │
  └─────────────────────────────────┘
       │
       ├── 批准 → waitForDecision()返回
       │          → 执行工具
       │          → SSE: tool_approval_result(approved)
       │
       └── 拒绝 → 工具不执行
                   → LLM收到"用户拒绝"消息
                   → 调整后续行为

八、面试高频问题

Q: 零配置注册是怎么实现的?
A: 利用Spring的依赖注入。AgentRegistry的构造函数接受 List<Agent> 参数,Spring自动收集所有实现了Agent接口且标注了@Component的Bean,注入到这个List中。注册器遍历List,以agentId为Key存入ConcurrentHashMap。新增Agent只需实现接口+@Component,Spring自动发现,无需手动注册。
Q: Pipeline和ReAct有什么区别?
A: Pipeline是预定义的固定流水线(如:解析→分析→审核),每个阶段的输入是前一阶段的输出,执行路径确定。ReAct是动态推理循环,LLM在运行时决定调用什么工具、循环多少次。Pipeline适合流程明确的任务,ReAct适合需要LLM自主决策的开放性任务。两者可以组合使用:Pipeline的某个阶段内部可以用ReAct来实现。
Q: HITL(人机协同)在什么场景下需要?
A: 当工具可能产生不可逆或高风险操作时,如:删除文件(shell_rm)、发送消息(给外部用户)、执行付款、修改数据库等。HITL机制让高风险工具在执行前暂停,等待人工审批后才继续。这保证了AI不会在无人监督的情况下执行危险操作。

九、VoiceAgent 语音通道 — 项目实战

VoiceAgent 提供语音输入 → 文本 → Agent → 语音输出的完整通道,支持 ASR(语音识别)和 TTS(语音合成)能力。

VoiceAgent 语音通道架构:

  ┌──────────┐     ┌────────────────────────┐     ┌──────────┐
  │  客户端    │────▶│  VoiceChannelController │────▶│  Agent   │
  │ (音频/文本) │◀────│  /api/voice/*          │◀────│  系统     │
  └──────────┘     └────────┬───────┬───────┘     └──────────┘
                            │       │
                      ┌─────▼──┐ ┌──▼─────┐
                      │  ASR   │ │  TTS   │
                      │ Service│ │ Service│
                      └────────┘ └────────┘

  前端入口:
  · 对话页面输入栏 🎤 录音按钮(仅语音服务可用时显示)
  · 点击录音 → 脉动红点 + 计时器 → 自动停止(120s)
  · 录音完成 → 自动调用 ASR → 识别文本填入输入框
  · 可用性检测: GET /api/voice/voices 探测

API 接口

方法路径说明
POST/api/voice/chat语音对话(音频输入 → Agent → 文本/音频输出)
POST/api/voice/asr语音转文本(Whisper API)
POST/api/voice/tts文本转语音(6种音色,0.25x~4.0x语速)
GET/api/voice/voices获取可用音色列表

关键设计

条件装配:ASR 和 TTS 可独立选择不同 Provider。通过 @ConditionalOnProperty 独立控制,OpenAI 实现会覆盖 NoOp 占位 Bean。例如 ASR 用 OpenAI Whisper,TTS 用 Edge 或阿里云。
兼容服务:修改 base-url 即可切换到 OpenAI 兼容服务(如阿里云 DashScope),无需改代码。
Q: VoiceAgent 的 ASR 和 TTS 是如何解耦的?
A: ASR(SpeechToTextService)和 TTS(TextToSpeechService)是两个独立接口,通过 hub.voice.asr-providerhub.voice.tts-provider 分别配置。采用 @ConditionalOnProperty 条件装配,NoOp 占位 Bean 保证即使不配置也不报错。新增提供商只需实现接口 + 加条件注解,零侵入。
Q: 前端如何检测语音服务是否可用?
A: 页面加载时发送 GET /api/voice/voices 探测请求,有返回则显示 🎤 按钮,无返回则隐藏。这种"能力探测"模式避免了硬编码配置,后端启停语音服务时前端自动适应。

十、DataAgent 数据分析 — NL2SQL 智能查询

DataAgent 是基于 NL2SQL(Natural Language to SQL)的数据分析 Agent,用户用自然语言提问,Agent 自动将问题转为 SQL 查询并解读结果。无需编写 SQL,对话即可查数据。

DataAgent 架构:

  DataAgent (extends AbstractLlmAgent)
       │
       │ LLM 推理时自动调用工具
       │
       ▼
  NL2SqlToolProvider (@Component, @ConditionalOnBean(DataSource.class))
       │
       ├── list_tables()      → DatabaseMetaData.getTables()
       ├── describe_table()   → DatabaseMetaData.getColumns() + getIndexInfo()
       └── execute_query()    → Statement.executeQuery() (只读, 超时30s)
             │
             ▼
         DataSource (JDBC — MySQL/PostgreSQL/Oracle/...)

工具链

工具名说明参数
list_tables获取数据库中所有可用的表名及注释,了解数据库全貌
describe_table获取指定表的列结构(列名、类型、可空、注释)、索引信息和前 3 行示例数据tableName
execute_query执行 SQL SELECT 查询,自动限制最多返回 100 行,禁止 DML/DDLsql

执行流程

NL2SQL 执行流程:

  用户: "最近7天注册了多少用户?"
       │
       ▼
  1. 理解需求 — Agent 分析查询意图
       │
       ▼
  2. 了解 Schema
       ├── 调用 list_tables → 发现 users 表
       └── 调用 describe_table("users") → 获取列结构
       │
       ▼
  3. 生成 SQL
       SELECT COUNT(*) FROM users
       WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
       │
       ▼
  4. 执行查询
       调用 execute_query(sql) → 只读模式 + 超时30s
       │
       ▼
  5. 解读结果
       "最近7天共注册了 42 位新用户。"
       + 附上 SQL 代码块 + 数据表格

安全机制

防护层措施实现
SQL 白名单仅允许 SELECT 语句必须以 SELECT 开头
禁止关键词拦截 INSERT/UPDATE/DELETE/DROP/ALTER/CREATE/TRUNCATE/GRANT/REVOKE/EXEC/CALL单词边界正则匹配 \b,去除引号内容后检查
表名校验防止 describe_table 参数注入正则 ^[a-zA-Z_][a-zA-Z0-9_]*$
只读连接数据库层面禁止写操作Connection.setReadOnly(true)
敏感脱敏password/secret/token/key 等列值自动替换为 [REDACTED]
结果限制防止大量数据返回最多 100 行,自动添加 LIMIT
超时控制防止慢查询查询 30s 超时,Agent 120s 超时
Q: NL2SQL 的安全性如何保证?
A: 多层防护:① SQL 必须以 SELECT 开头;② 使用单词边界正则(\b)拦截 INSERT/DELETE 等关键词,且排除引号内的内容避免误杀;③ 表名参数严格正则校验;④ 连接设为 ReadOnly;⑤ 敏感列自动脱敏。即使 LLM 被 prompt injection 诱导生成危险 SQL,也会被工具层拦截。
Q: DataAgent 为什么先查 Schema 再生成 SQL?
A: 系统提示词要求 Agent "先查后写"。LLM 本身不知道真实的数据库表名和列名,如果直接生成 SQL 大概率出错。通过 list_tablesdescribe_table 工具获取真实 Schema(包括列名、类型、注释、示例数据),LLM 能生成准确的 SQL,大幅减少查询失败率。