# SpringAIProject **Repository Path**: harry_software/SpringAIProject ## Basic Information - **Project Name**: SpringAIProject - **Description**: Spring AI Project - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: release.v1.0 - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-06-03 - **Last Updated**: 2026-09-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SpringAIProject 基于 **Spring Boot 3.5** 与 **Spring AI 1.1** 的后端示例项目,演示 MyBatis-Plus、Redis 与**多 LLM 提供商统一接入**(DeepSeek、通义千问、OpenAI、Ollama 等)的配置化共存与动态路由,并集成 **JDBC 多轮对话记忆**、**动态工具发现**(Function Calling)以及 **RAG 检索增强生成**(知识库、文档解析、pgvector 向量化、问答)。配套 **Vue 3 管理前台**(`portalUI/`)覆盖知识库 CRUD、文档管理、向量化与 RAG 问答全流程。 ## 功能特性 - **多模型统一配置**:在 `spring.ai.provider` 下声明各提供商与模型参数 - **多模型共存**:仅对 `enabled=true` 且凭证有效的 provider 创建 `ChatClient`,缺 Key 不影响启动 - **动态路由**:支持按 `provider` 或 `model` 选择模型,可运行时切换默认提供商 - **JDBC多轮记忆**:通过 `conversationId` 关联上下文,滑动窗口保留最近 N 条消息 - **动态工具发现**:Lucene 索引 + Tool Search,按需检索天气/订单/时间等工具(仅同步对话) - **流式对话**:多模型动态流式(Stream)对话接口与 DeepSeek 专用接口 - **RAG知识库**:多知识库管理、PDF/Word/Excel上传解析、Token分块、pgvector向量存储 - **RAG问答**:相似度检索([显式向量化 + SQL]方式 或 [Spring AI VectorStore]方式) + 多模型 LLM 生成答案(配置提示词) - **向量化服务**:文档异步向量化、实时文本向量化 - **管理前台**:`portalUI/` Vue 3 单页应用管理界面 - **Prompt、Context、Harness 三层工程体系**:Context 收集运行时数据 → Prompt 模板化组装 → Harness 多智能体编排(多模型路由、记忆、动态工具、内容过滤、弹性执行) - **统一异常体系**:Spring AI结合resilience4j构建异常统一体系,弹性配置双层重试(熔断)策略[应用层重试+ HTTP层重试]、限流、内容过滤(主动检测+拦截响应处理策略)、超时;**按 模型供应商-provider分类 自动重试(熔断)、限流**(Resilience4j) - **RAG ETL 流水线**:索引 + 向量化 + 检索 + 生成答案 - **多轮RAG Chat与普通Chat**:增加多轮RAG Chat与多轮普通Chat - **测试集与评估机制**:增加测试集Test Set与评估机制Evaluation Pipeline - **Agent架构(P0→P1→P2→P3)**:任务拆解 → 执行调度 → 结果回传;P1 增 RAG/重规划/落库;P2 增异步 SSE/人工审批/并行/前台; P3增加专家-Specialist/轨迹评估-Eval/成本仪表盘(详见 [迭代设计章节](#agent-编排p0--p1--p2--p3-迭代设计与主流程)) - **Skills能力封装**:检索、问答、流程触发、数据查询等能力模块化 - **基础能力**:健康检查、MyBatis-Plus CRUD 示例、Knife4j API 文档 ## 技术栈 | 类别 | 技术 | |---|---| | 语言 / 框架 | Java 21、Spring Boot 3.5.7、Spring AI 1.1.7 | | 大模型 | DeepSeek、OpenAI、Ollama、通义千问(DashScope,`spring-ai-alibaba` 1.1.2.3) | | 对话记忆 | `spring-ai-starter-model-chat-memory-repository-jdbc`(JDBC 持久化) | | 工具发现 | `tool-search-tool` 1.0.1、`tool-searcher-lucene` 1.0.1 | | **RAG** | `spring-ai-starter-vector-store-pgvector` 1.1.7、PostgreSQL + pgvector、Apache PDFBox 3.0.3 | | 数据 | MySQL 8、MyBatis-Plus 3.5.9、Redis、Redisson 3.35.0、PostgreSQL 42.7.4 | | 文档 / 工具 | Knife4j 3.0.3、Lombok、EasyExcel 4.0.3、Apache POI 5.2.3 | | **前台** | Vue 3.5、Vite 6、Element Plus 2.9、Axios(`portalUI/`) | 更完整的版本与约定见 [AGENTS.md](AGENTS.md)。 ## 系统功能界面 ![输入图片说明](sysIndex.jpg) ## 架构概览 ``` application.yml (spring.ai.provider + app.llm.memory) │ ▼ AiProviderProperties ──► AiProviderChatModelFactory ──► Map │ │ ▼ ▼ AiProviderRouter ◄────────────────────────────────── LlmChatService(薄编排) │ ├── LlmPromptPipeline(Context + Prompt) │ └── AiChatHarness(多智能体编排) │ ├── MessageChatMemoryAdvisor │ ├── ToolSearchToolCallAdvisor │ └── OrderTool / TimeTool / WeatherTool ▼ LlmChatController / DeepSeekChatController ``` **路由优先级**(`AiProviderRouter`): 1. 请求参数 `provider` 2. 请求参数 `model`(查 `routing` 表或 `providers.*.model`) 3. 当前默认 provider(配置或 `POST /api/llm/switch` 运行时切换) 核心类位于 `com.springai.harrycj.common.config` 与三层工程包:`common.context`、`common.prompt`、`common.harness`(详见 [Prompt / Context / Harness 三层工程体系](#prompt--context--harness-三层工程体系))。 LLM 调用链经 **Context → Prompt → Harness** 三层工程体系到达 `ChatClient`;异常由 **统一异常体系**(`common.exception` + `common.moderation` + `common.resilience`)集中处理,详见下文 [Prompt / Context / Harness 三层工程体系](#prompt--context--harness-三层工程体系) 与 [统一 AI 异常体系](#统一-ai-异常体系)。 多步任务编排见 [Agent 编排:P0 → P1 → P2 → P3 迭代设计与主流程](#agent-编排p0--p1--p2--p3-迭代设计与主流程)(在 Harness 之上的 Orchestrator 层)。 ### RAG 架构 库设计:**PostgreSQL(pgvector)存 知识库/文档元数据、 文档向量**。 ``` portalUI (Vue 3) ──HTTP──► /api/rag/* │ ┌───────────────────────┼───────────────────────┐ ▼ ▼ ▼ KnowledgeBaseController RagDocumentController RagAskController │ │ │ ▼ ▼ ▼ KnowledgeBaseService DocumentService RagAskService │ RagDocumentParser │ │ │ RagRetriever ──► PgVectorOperations │ ▼ │ (pgvector) │ VectorizationService ▼ │ │ RagGenerator ──► RagPromptPipeline → AiChatHarness │ │ │ → AiProviderRouter → ChatClient ▼ ▼ PostgreSQL 本地文件 + PostgreSQL (rag_knowledge_base, (./data/rag/{kbId}/) rag_document) ``` **文档入库链**:上传 → 解析分块(PDF/Word/Excel)→ 向量化 → 写入 pgvector。 **问答链**:问题嵌入 → 相似度检索 → `RagPromptPipeline`(Context + Prompt)→ `AiChatHarness` 多智能体编排 → LLM 生成答案。 ### Prompt / Context / Harness 三层工程体系 将 LLM 调用拆为 **Context(数据)→ Prompt(模板)→ Harness(多智能体编排)** 三层,业务 Service 只负责 HTTP 参数与流程编排,避免 Prompt 硬编码与 ChatClient 调用逻辑散落各处。 > **概念区分**:宏观三层(本节)与 Prompt 层内部的 `PromptLayer`(SYSTEM / BUSINESS / USER 拼接)是不同粒度——后者是 Prompt 层内的模板渲染策略。 #### 宏观三层分工 | 层 | 包 | 职责 | 输入 → 输出 | |---|---|---|---| | **Context** | `common.context`、`business.rag.context` | 收集运行时事实(用户输入、RAG 检索片段、业务变量) | 业务参数 → `PromptContext` | | **Prompt** | `common.prompt` | 模板加载与层次化拼接(SYSTEM / BUSINESS / USER) | `PromptContext` → `PromptAssemblyResult` | | **Harness** | `common.harness` | 多智能体编排:多模型路由、记忆、动态工具、内容过滤、弹性执行 | `HarnessRequest` + `HarnessProfile` → 模型回复 | #### 整体架构 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ Service 层(薄编排) │ │ LlmChatService / DeepSeekChatService / RagGenerator │ └───────────────────────────────┬─────────────────────────────────────────┘ │ ┌─────────────────────┴─────────────────────┐ ▼ ▼ ┌──────────────────────────┐ ┌──────────────────────────┐ │ Context 层 │ │ Context 层(RAG 领域) │ │ LlmPromptPipeline │ │ RagPromptPipeline │ │ ├─ LlmChatContextBuilder │ │ ├─ RagAskContextBuilder │ │ └─ → PromptContext │ │ └─ → RagAskContext │ └─────────────┬────────────┘ └─────────────┬────────────┘ │ │ └─────────────────┬───────────────────────┘ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ Prompt 层 │ │ PromptContextAssembler + AiPromptTemplateService │ │ SYSTEM(角色/约束)→ BUSINESS(检索上下文等)→ USER(用户问题) │ │ ▼ │ │ PromptAssemblyResult(systemMessage + userMessage) │ └───────────────────────────────┬─────────────────────────────────────────┘ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ Harness 层(多智能体编排) AiChatHarness │ │ HarnessRequest + HarnessProfile │ │ ├─ 多模型路由(AiProviderRouter → ChatClient) │ │ ├─ JDBC 记忆(MessageChatMemoryAdvisor,按 Profile 启用) │ │ ├─ 动态工具发现(ToolSearchToolCallAdvisor + Order/Time/WeatherTool) │ │ ├─ Prompt 注入(.system() / .user()) │ │ └─ 治理链(AiContentFilterGuard → AiCallResilienceExecutor → 模型调用) │ └─────────────────────────────────────────────────────────────────────────┘ ``` #### Context 层 | 包 / 类 | 职责 | |---|---| | `common.context.LlmChatContextBuildRequest` | 通用 LLM Context 入参(`userMessage` + 可扩展 `businessData` / `variables`) | | `common.context.LlmChatContextBuilder` | 构建 `PromptContext` | | `common.context.LlmPromptPipeline` | Context 构建 → `assembleLlm()` | | `business.rag.context.RagAskContext` | RAG Context 结果(`PromptContext` + `kbSystemPromptOverride`) | | `business.rag.context.RagAskContextBuilder` | 检索片段格式化、`kbName` 等变量注入 | | `business.rag.context.RagPromptPipeline` | Context 构建 → `assembleRag()` | #### Prompt 层(PromptLayer 微观拼接) | 层次 | 枚举 | 映射消息 | 典型内容 | 示例变量 | |---|---|---|---|---| | **系统指令** | `SYSTEM` | `ChatClient.system()` | 角色定义、回答规范、全局约束 | `{kbName}` | | **业务数据** | `BUSINESS` | 注入 user 模板变量 | RAG 检索片段、订单号、用户信息 | `{context}`、`{businessContext}` | | **用户输入** | `USER` | `ChatClient.user()` | 用户原始问题或指令 | `{userInput}`、`{question}` | | 包 / 类 | 职责 | |---|---| | `common.prompt.PromptLayer` | 微观三层枚举:SYSTEM / BUSINESS / USER | | `common.prompt.PromptContext` | 用户输入 + 业务数据 + 扩展变量容器 | | `common.prompt.PromptAssemblyResult` | 组装结果(systemMessage + userMessage) | | `common.prompt.AiPromptTemplateService` | classpath / 内联模板,Spring AI `PromptTemplate` 渲染 | | `common.prompt.PromptContextAssembler` | `assembleLlm()` / `assembleRag()` | | `common.config.LlmPromptProperties` | `app.llm.prompt` 配置 | | `common.config.RagProperties.Prompt` | `app.rag.prompt` 配置 | `PromptContext.toRenderVariables()` 自动合并变量,并生成 `{businessContext}` 汇总块(优先取 `context` 键)。 #### Harness 层(多智能体编排) `AiChatHarness` 是模型调用的**统一执行入口**,按 `HarnessProfile` 组合 Advisor 链与治理策略: | Profile | 记忆 | 动态工具 | 典型场景 | |---|---|---|---| | `LLM_CHAT` | ✓ | ✓ | `GET /api/llm/chat` 同步多轮对话 | | `LLM_STREAM` | ✓ | ✗ | `GET /api/llm/chat/stream` 流式对话 | | `RAG_ASK` | ✗ | ✗ | `POST /api/rag/ask` 检索增强问答 | | `RAG_CHAT` | ✓ | ✗ | `POST /api/rag/chat` 多轮检索增强对话 | | `SIMPLE` | ✗ | ✗ | DeepSeek 专用 API | | `AGENT_PLAN` | ✗ | ✗ | Agent 任务拆解(结构化 JSON) | | `AGENT_STEP` | ✗ | ✗ | Agent 步骤推理 / 结果汇总 | | `AGENT_TOOL` | ✗ | ✓ | Agent 工具步骤(动态工具发现) | | 包 / 类 | 职责 | |---|---| | `common.harness.HarnessRequest` | 统一请求(provider、model、assembled、rawUserInput、conversationId) | | `common.harness.HarnessProfile` | 执行策略枚举 | | `common.harness.AiChatHarness` | 路由 → Advisor 链 → Prompt 注入 → 内容过滤 → 同步/流式调用 | **多智能体**在此指 Harness 层统一编排的多种「智能体能力」:多 LLM 提供商路由、带记忆的对话智能体、Lucene 按需检索的工具调用智能体(OrderTool / TimeTool / WeatherTool),而非多个独立 Agent 进程。 #### 标准调用链 | 模块 | Context | Prompt | Harness | |---|---|---|---| | 统一对话 | `LlmPromptPipeline.assemble(message)` | (Pipeline 内) | `AiChatHarness.chat/stream(..., LLM_CHAT / LLM_STREAM)` | | DeepSeek 专用 | 同上 | 同上 | `AiChatHarness.chat/stream(..., SIMPLE)` | | RAG 问答 | `RagPromptPipeline.assemble(...)` | 同上 | `AiChatHarness.chat(..., RAG_ASK)` | | RAG Chat | `RagPromptPipeline.assemble(...)` | 同上 | `AiChatHarness.chat(..., RAG_CHAT)` | | Agent 编排 | `AgentPromptPipeline`(plan/step/aggregate) | (Pipeline 内) | `AGENT_PLAN` / `AGENT_TOOL` / `AGENT_STEP` | **RAG system 优先级**(高 → 低): 1. 知识库 `rag_knowledge_base.system_prompt`(支持 `{kbName}` 等占位符) 2. `app.rag.prompt.system-template`(classpath 模板) 3. `app.rag.prompt.default-system-prompt`(内联兜底) #### 模板文件 | 路径 | 用途 | |---|---| | `src/main/resources/prompts/llm/default-system.st` | 通用 LLM 对话 system 模板 | | `src/main/resources/prompts/llm/default-user.st` | 通用 LLM 对话 user 模板(`wrap-user-input=true` 时生效) | | `src/main/resources/prompts/rag/default-system.st` | RAG 问答 system 模板(含 `{kbName}`) | | `src/main/resources/prompts/rag/default-user.st` | RAG 问答 user 模板(含 `{context}`、`{question}`) | | `src/main/resources/prompts/agent/plan-system.st` | Agent 任务拆解 system 模板 | | `src/main/resources/prompts/agent/step-system.st` | Agent 步骤执行 system 模板 | | `src/main/resources/prompts/agent/aggregate-system.st` | Agent 结果汇总 system 模板 | | `src/main/resources/prompts/agent/replan-system.st` | Agent 失败重规划 system 模板 | 模板占位符使用 Spring AI 标准 `{variable}` 语法;配置项支持 `classpath:prompts/...` 或内联文本。 #### 配置 ```yaml app: llm: prompt: enabled: true system-template: classpath:prompts/llm/default-system.st user-template: classpath:prompts/llm/default-user.st wrap-user-input: false # false=用户消息原样透传;true=经 user-template 包装 rag: prompt: system-template: classpath:prompts/rag/default-system.st user-template: classpath:prompts/rag/default-user.st ``` | 配置项 | 说明 | |---|---| | `app.llm.prompt.enabled` | 是否启用 LLM 模板化;`false` 时 user 原样透传、不注入 system | | `app.llm.prompt.system-template` | 通用对话 system 模板路径或内联文本 | | `app.llm.prompt.user-template` | 通用对话 user 模板(需 `wrap-user-input=true`) | | `app.llm.prompt.wrap-user-input` | 是否用模板包装用户输入 | | `app.rag.prompt.system-template` | RAG system 模板(KB 未配置 `system_prompt` 时使用) | | `app.rag.prompt.user-template` | RAG user 模板(注入 `{context}`、`{question}`) | #### 横切能力边界 - **JDBC 记忆**:Harness 层按 Profile 注入 `MessageChatMemoryAdvisor`;与 system prompt 独立共存(system 在前、history 在中间、当前 user 在后) - **动态工具**:Harness 层在 `LLM_CHAT` / `AGENT_TOOL` Profile 下注册 tools + `ToolSearchToolCallAdvisor`;system 模板可引导模型优先使用工具 - **内容过滤**:`AiContentFilterGuard` 在 Harness 内串联,对**原始用户输入**做 INPUT 检测,对**模型完整回复**做 OUTPUT 拦截,不参与 Context / Prompt 构建 - **弹性执行**:`AiCallResilienceExecutor`(限流 / 熔断 / 重试)经 Guard 间接调用,详见 [统一 AI 异常体系](#统一-ai-异常体系) ### Agent 编排:P0 → P1 → P2 → P3 迭代设计与主流程 本模块在现有 **Context → Prompt → Harness** 三层之上增加 **Orchestrator(编排层)**,按 **先闭环、再增强、后运营、再生产增强** 分四期交付。理解迭代思想时,抓住一条主线: > **Harness 始终只负责「一次 LLM/工具调用」;多步状态、调度、审批、事件推送、专家-Specialist 委派全部放在 Agent Orchestrator。** #### 设计定位与边界 | 层 | 职责 | Agent 中的角色 | |---|---|---| | Context / Prompt | 组装 system / business / user | Planner / Step / Aggregate / Replan 模板 | | Harness(`AiChatHarness`) | 路由、记忆、工具、过滤、弹性 | **原子执行引擎**(每步调用) | | Agent Orchestrator(新建) | 拆解、决策、调度、回传 | **多步任务大脑** | 推荐范式:**Plan-and-Execute(主)+ 失败重规划(辅)+ 可选人工闸门 + Specialist 委派**。不引入独立工作流引擎;首版用轻量状态机 + 依赖调度即可。 #### 迭代总览(为何这样拆) ``` P0 跑通闭环 Plan → 串行 Execute(TOOL/LLM) → Aggregate └─ 证明「编排层」可行,API / Prompt / Profile 定型 P1 对齐业务 + RAG Skill + 失败 REPLAN + ToolRegistry + MySQL 落库 └─ 可检索、可纠错、可插拔工具、可审计 P2 可运营 + 异步/SSE + 人工审批 + dependsOn 并行 + portalUI └─ 长任务不阻塞、可观测、可介入、可并行提效 P3 生产增强 + Specialist 子 Agent + 轨迹 Eval + 成本/步数仪表盘 └─ 角色分工、质量度量、成本可感知 ``` | 期次 | 一句话目标 | 刻意不做(留给后续) | |--------|---|---| | **P0** | 同步四段闭环可演示 | RAG、重规划、落库、异步、审批、并行 | | **P1** | 可检索、可纠错、可插拔工具、可审计 | RAG Skill + 失败 REPLAN + ToolRegistry + MySQL 落库 | | **P2** | 业务可用 + 可审计 | 异步 SSE、人工审批、前台、依赖并行 | | **P3** | 业务可用 + 可审计 | Specialist、轨迹 Eval、成本仪表盘 | #### 能力对照矩阵 | 能力 | P0 | P1 | P2 | P3 | |---|---|---|---|---| | 任务拆解(Planner → JSON Plan) | ✓ | ✓ | ✓ | ✓ | | 串行执行 TOOL / LLM | ✓ | ✓ | ✓ | ✓ | | 结果汇总 Aggregate | ✓ | ✓ | ✓ | ✓ | | RAG Skill(`RagRetriever`) | | ✓ | ✓ | ✓ | | 失败重规划 REPLAN | | ✓ | ✓ | ✓ | | `DynamicToolRegistry` 解耦工具 | | ✓ | ✓ | ✓ | | MySQL 步骤落库 | | ✓ | ✓ | ✓ | | `dependsOn` 依赖调度 | | | ✓ | ✓ | | 无依赖步骤并行 | | | ✓ | ✓ | | 异步提交 + SSE 事件 | | | ✓ | ✓ | | 人工审批(计划 / APPROVAL) | | | ✓ | ✓ | | portalUI `/agent` 任务页 | | | ✓ | ✓ | | SPECIALIST 子 Agent 委派 | | | | ✓ | | Token / 成本采集与估算 | | | | ✓ | | 轨迹 Eval(关键词 + 轨迹分) | | | | ✓ | | 成本/步数仪表盘 API + 前台 | | | | ✓ | #### 总体架构(P3 终态,向下兼容 P0/P1/P2) ``` ┌──────────────────────────────────────────────────────────────────────────┐ │ portalUI /agent · /agent/dashboard · /agent/eval 或 API 客户端 │ │ POST /run | /run/async | GET /dashboard/summary | /eval/* | /specialists │ └───────────────────────────────┬──────────────────────────────────────────┘ │ ┌───────────────────────────────▼──────────────────────────────────────────┐ │ AgentOrchestratorService(主编排) │ │ Goal → Plan → [Approval?] → Schedule → Execute → Reflect/Replan → Reply │ └───┬──────────┬──────────┬──────────┬──────────┬──────────┬───────────────┘ ▼ ▼ ▼ ▼ ▼ ▼ Planner Decision Scheduler StepExecutor EventBus TaskStore (拆解) (REPLAN) (依赖/并行) (TOOL/RAG/LLM/SPECIALIST) (SSE) (MySQL) │ │ │ │ │ │ │ ├─ SpecialistAgentRegistry │ │ │ └─ AgentCostEstimator └──────────┴──────────┴──────────┘ ▼ AiChatHarness.chatDetailed + HarnessProfile.AGENT_* ▼ AiProviderRouter / DynamicToolRegistry / RagRetriever / 治理链 ``` **核心类(按迭代引入)** | 类 | 引入期 | 职责 | |---|---|---| | `AgentOrchestratorService` | P0(P2 大幅扩展) | 主编排入口 | | `AgentPlanner` | P0(P1 增 replan;P3 增 SPECIALIST) | 结构化拆解 / 重规划 | | `AgentStepExecutor` | P0(P1 增 RAG;P3 增 SPECIALIST + 成本) | 单步执行 | | `AgentPromptPipeline` | P0(P1 增 replan 模板) | plan/step/aggregate/replan 渲染 | | `RagAgentSkill` | P1 | 知识库检索 Skill | | `AgentDecisionEngine` | P1 | 失败后 CONTINUE / REPLAN | | `DynamicToolRegistry` | P1 | 扫描 `@Tool` Bean,解耦 Harness | | `AgentTaskStore` | P1 | `agent_task` / `agent_task_step` | | `AgentStepScheduler` | P2 | 按 `dependsOn` 选就绪步骤 | | `AgentEventBus` | P2 | 内存 SSE 事件广播 | | `AgentRuntimeState` | P2 | 异步 / 审批挂起运行时状态 | | `SpecialistAgentRegistry` / `BuiltinSpecialists` | P3 | 多角色子 Agent | | `AgentCostEstimator` | P3 | Token → USD 估算 | | `AgentEvalPipelineService` | P3 | 轨迹评估流水线 | | `AgentDashboardService` | P3 | 成本 / 步数聚合 | **HarnessProfile(Agent 相关)** | Profile | 记忆 | 工具 | 用途 | |---|---|---|---| | `AGENT_PLAN` | ✗ | ✗ | 拆解 / 重规划(强制 JSON) | | `AGENT_STEP` | ✗ | ✗ | 步骤推理 / 汇总 / 多数 Specialist | | `AGENT_TOOL` | ✗ | ✓ | 工具步骤 / TOOL_OPERATOR Specialist | #### P0 — 跑通闭环(主流程) **交付思想**:先证明「目标 → 计划 → 执行 → 回传」四段成立;同步阻塞 API,技能仅 TOOL + LLM,轨迹只在响应体返回。 ``` POST /api/agent/run { goal } │ ▼ AgentPlanner(AGENT_PLAN)──► AgentPlan{ steps: TOOL|LLM } │ ▼ 串行 for each step ├─ TOOL → AGENT_TOOL(Order/Time/Weather + ToolSearch) └─ LLM → AGENT_STEP │ ▼ Aggregate(AGENT_STEP)──► AgentRunVo{ plan, steps, answer, metrics } ``` **关键约定** - Plan 解析失败 → 有限次重试 → 仍失败则**降级为单步 LLM**(保证接口始终有结果)。 - 同步接口适合演示与短任务;不挂起、不落库。 - Prompt:`prompts/agent/plan-system.st`、`step-system.st`、`aggregate-system.st`。 **验证示例** ```bash curl -X POST http://localhost:8080/api/agent/run \ -H "Content-Type: application/json" \ -d "{\"goal\":\"查一下订单ORD12345678的状态,并用一句话总结\",\"maxSteps\":5}" ``` #### P1 — 业务增强(主流程) **交付思想**:在 P0 闭环上补齐真实业务三要素——**检索增强、失败可纠错、过程可审计**;工具注册从 Harness 硬编码改为自动发现。 ``` POST /api/agent/run { goal, knowledgeBaseId? } │ ▼ AgentPlanner(可用技能含 RAG,当传入 knowledgeBaseId) │ ▼ 串行执行 ├─ TOOL → DynamicToolRegistry + AGENT_TOOL ├─ RAG → RagAgentSkill → RagRetriever(不经 LLM 直出片段) └─ LLM → AGENT_STEP │ ├─ 步骤 FAILED 且 replanCount < max-replans │ → AgentDecisionEngine.REPLAN │ → AgentPlanner.replan(丢弃未完成步,保留已成功结果) │ → 继续执行新 pending │ ▼ Aggregate → 落库 AgentTaskStore → AgentRunVo GET /api/agent/tasks/{taskId} ← 可事后查询轨迹 ``` **相对 P0 的增量** | 增量 | 说明 | |---|---| | `AgentStepType.RAG` | 请求带 `knowledgeBaseId` 时 Planner 才允许输出 RAG 步 | | `AgentPlanner.replan` | 失败原因 + 已完成摘要 → 剩余步骤 JSON | | `DynamicToolRegistry` | 新增 `@Component`+`@Tool` 即可被 Agent/LLM_CHAT 使用 | | MySQL 落库 | `db/agent-schema.sql`;`app.agent.store.initialize-schema=true` 可自动建表 | **配置要点** ```yaml app: agent: max-replans: 2 skills: { tool: true, rag: true, llm: true } store: { persist-steps: true, initialize-schema: true } ``` #### P2 — 运营增强(主流程) **交付思想**:长任务不能阻塞 HTTP;要**可观测(SSE)**、**可介入(审批)**、**可提效(并行)**、**可操作(前台)**。 ``` 异步路径: POST /run/async ──立即返回──► { taskId, streamUrl } │ │ │ 后台线程 └─► GET /tasks/{id}/stream (SSE) ▼ PLAN_READY ──(requirePlanApproval?)──► WAITING_HUMAN │ │ │◄──── POST /tasks/{id}/resume ────────┘ ▼ AgentStepScheduler.nextReady(dependsOn) ├─ 无依赖且 parallel-steps=true → 线程池并行 ├─ APPROVAL 步骤 → 再次 WAITING_HUMAN └─ 依赖失败 → SKIPPED ▼ STEP_* / REPLAN 事件推送 → Aggregate → TASK_FINISHED → 落库 同步路径 POST /run 仍可用(忽略计划审批挂起,适合短任务)。 ``` **SSE 事件(`AgentEventType`)** | 事件 | 含义 | |---|---| | `TASK_STARTED` | 任务提交 / 审批后继续 | | `PLAN_READY` | 计划生成完成 | | `STEP_STARTED` / `STEP_FINISHED` | 批次开始 / 单步结束 | | `REPLAN` | 失败后已重规划 | | `WAITING_HUMAN` | 等待审批(计划或 APPROVAL 步) | | `TASK_FINISHED` / `ERROR` | 终态 | **任务状态机** ``` RUNNING ──► WAITING_HUMAN ──(approved)──► RUNNING ──► SUCCEEDED | PARTIAL | FAILED │ └──(rejected / timeout)──► CANCELLED ``` **portalUI**:路由 `/agent`(`AgentTaskPage.vue`)— 异步提交、事件流、批准/拒绝、历史任务回看。 **验证示例** ```bash # 异步 + 计划审批 + 并行 curl -X POST http://localhost:8080/api/agent/run/async \ -H "Content-Type: application/json" \ -d "{\"goal\":\"查订单ORD12345678并总结\",\"requirePlanApproval\":true,\"parallelSteps\":true}" # SSE(另开终端) curl -N http://localhost:8080/api/agent/tasks/{taskId}/stream # 批准继续 curl -X POST http://localhost:8080/api/agent/tasks/{taskId}/resume \ -H "Content-Type: application/json" \ -d "{\"approved\":true,\"comment\":\"ok\"}" ``` #### P3 — 生产增强(主流程) **交付思想**:多角色分工、质量可度量、成本可感知。Specialist 仍是 Orchestrator 调度的一步,内部再调 Harness;Eval / 仪表盘消费落库轨迹与 metrics。 ``` Planner 可输出 专家-SPECIALIST 步(specialist=RESEARCHER|TOOL_OPERATOR|WRITER|GENERAL) │ ▼ StepExecutor → SpecialistAgentRegistry.require(role) → 子 Agent.run() │ RESEARCHER:可选 RAG + AGENT_STEP │ TOOL_OPERATOR:AGENT_TOOL │ WRITER / GENERAL:AGENT_STEP │ ▼ Harness.chatDetailed → ChatInvocationResult{ content, prompt/completion tokens } │ ▼ AgentCostEstimator(app.agent.pricing)→ step.estimatedCostUsd / metrics.totalTokens Eval: POST /api/agent/eval/runs { testSetId } → 逐用例 orchestrator.run(goal) → 关键词重叠(RagEvalKeywordScorer)+ 轨迹成功率 → 落库 agent_eval_* 仪表盘: GET /api/agent/dashboard/summary?limit=50 → 聚合 agent_task.metrics_json(步数 / Token / 成本 / 状态分布) ``` **内置 Specialist** | 角色 | 说明 | |---|---| | `RESEARCHER` | 优先知识库检索并整理研究摘要 | | `TOOL_OPERATOR` | 专注 Function Calling / 动态工具 | | `WRITER` | 基于已有事实撰写答复 | | `GENERAL` | 通用推理 | **portalUI**:`/agent/dashboard`(成本步数)、`/agent/eval`(轨迹评估);任务页展示 metrics / Specialist。 **验证示例** ```bash # Specialist 列表 curl http://localhost:8080/api/agent/specialists # 仪表盘 curl "http://localhost:8080/api/agent/dashboard/summary?limit=50" # 创建评估测试集 + 用例 + 运行 curl -X POST http://localhost:8080/api/agent/eval/test-sets \ -H "Content-Type: application/json" \ -d "{\"name\":\"smoke\",\"description\":\"P3\"}" curl -X POST http://localhost:8080/api/agent/eval/test-sets/1/cases \ -H "Content-Type: application/json" \ -d "{\"goal\":\"现在几点了\",\"expectedAnswer\":\"时间\"}" curl -X POST http://localhost:8080/api/agent/eval/runs \ -H "Content-Type: application/json" \ -d "{\"testSetId\":1,\"maxSteps\":4}" ``` #### 主流程对照(一眼看清演进) | 阶段 | 入口 | 调度 | 技能 | 失败策略 | 回传 | |---|---|---|---|---|---| | P0 | `POST /run` 同步 | 严格串行 | TOOL, LLM | 记 FAILED,继续或结束 | 响应体轨迹 | | P1 | `POST /run` 同步 | 严格串行 | + RAG | REPLAN(有上限) | 响应体 + MySQL | | P2 | `/run` 或 `/run/async` | dependsOn + 可选并行 | + APPROVAL | REPLAN + 人工闸门 | 响应体 + MySQL + SSE | | P3 | 同上 + `/eval` `/dashboard` | 同上 | + SPECIALIST | 同上 | + Token/成本 + Eval | #### 配置与包结构速查 ```yaml app: agent: enabled: true max-steps: 8 max-replans: 2 parallel-steps: true parallel-pool-size: 4 skills: { tool: true, rag: true, llm: true, specialist: true } store: { persist-steps: true, initialize-schema: true } pricing: default-input-per-1k: 0.001 default-output-per-1k: 0.002 eval: enabled: true initialize-schema: true keyword-overlap-threshold: 0.3 enable-keyword-overlap: true enable-trajectory-score: true prompt: plan-system-template: classpath:prompts/agent/plan-system.st step-system-template: classpath:prompts/agent/step-system.st aggregate-system-template: classpath:prompts/agent/aggregate-system.st replan-system-template: classpath:prompts/agent/replan-system.st ``` ``` business/agent/ ├── controller/ # REST + SSE + specialists + dashboard ├── service/ # AgentOrchestratorService ├── planner/ # 拆解 / 重规划 ├── executor/ # 单步执行(含 SPECIALIST) ├── specialist/ # SpecialistAgentRegistry + BuiltinSpecialists ├── cost/ # AgentCostEstimator ├── eval/ # 轨迹评估(test-set / pipeline / schema) ├── dashboard/ # 成本/步数聚合 ├── decision/ # 失败决策 ├── scheduler/ # dependsOn 就绪集 ├── skill/ # RagAgentSkill ├── event/ # AgentEventBus ├── runtime/ # 异步/审批内存态 ├── store/ # MySQL 落库 ├── context/ # AgentPromptPipeline ├── model/ | agentapi/ | entity/ | mapper/ ``` > **后续方向**:Redis 跨实例事件总线、LLM-as-Judge(`app.agent.eval.enable-llm-judge`)、更细粒度按模型定价表等。 --- ### 统一 AI 异常体系 覆盖模型调用 **超时**、**限流**、**内容过滤**、**重试、熔断** 等场景,将 Spring AI 底层异常与本地业务异常统一翻译为结构化 JSON 响应。 #### 整体架构 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ Controller 层 │ │ LlmChatController / DeepSeekChatController / RagAskController / Agent… │ └───────────────────────────────┬─────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ Service 层(薄编排) │ │ LlmChatService / RagGenerator / AgentOrchestratorService / … │ └───────────────────────────────┬─────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ Harness 层 AiChatHarness(同步/流式模型调用入口) │ └───────────────────────────────┬─────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 内容过滤守卫 AiContentFilterGuard │ │ ┌──────────────┐ ┌──────────────────────────────────┐ ┌───────────┐ │ │ │ 主动检测 INPUT│ → │ 弹性执行 AiCallResilienceExecutor │→│拦截 OUTPUT│ │ │ │ checkInput │ │ ① RateLimiter(本地控速) │ │checkOutput│ │ │ └──────────────┘ │ ② CircuitBreaker(按供应商-provider) │ └───────────┘ │ │ │ ③ 自动重试(指数退避 / 429 延迟) │ │ │ │ → ChatClient / EmbeddingModel │ │ │ └──────────────────────────────────┘ │ │ ↑ AiContentFilterService ↑ Resilience4j RateLimiter + CB │ └───────────────────────────────┬─────────────────────────────────────────┘ │(异常) ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 异常翻译 AiExceptionTranslator │ │ TransientAiException / NonTransientAiException / HTTP 4xx·5xx / … │ │ → AiServiceException(AiModelTimeout / AiRateLimit / AiContent…) │ └───────────────────────────────┬─────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 全局处理 GlobalAiExceptionHandler → ApiErrorResponse(JSON) │ └─────────────────────────────────────────────────────────────────────────┘ ``` `AiCallResilienceExecutor` 执行顺序:**限流 → 熔断 → 重试 → 实际调用**。`resilience.enabled=false` 时跳过弹性策略,仅经 `AiExceptionTranslator.execute()` 翻译异常。 #### 核心包与类 | 包 / 类 | 职责 | |---|---| | `common.exception.AiErrorCode` | 错误码枚举,映射 HTTP 状态与 `retryable` | | `common.exception.AiServiceException` | 统一业务异常基类 | | `common.exception.AiModelTimeoutException` | 模型调用超时 | | `common.exception.AiRateLimitException` | 限流(`rateLimitSource=LOCAL` 本地控速 / `PROVIDER` 上游 429) | | `common.exception.AiContentFilterException` | 内容安全未通过(含 `phase` / `source`) | | `common.exception.AiCircuitBreakerOpenException` | 按 provider 熔断器 OPEN | | `common.exception.AiExceptionTranslator` | 底层异常 → 统一异常;提供 `execute()` 包装 | | `common.exception.GlobalAiExceptionHandler` | `@RestControllerAdvice` 全局 JSON 响应;流式错误 `toStreamErrorPayload()` | | `common.exception.ApiErrorResponse` | 统一错误响应体(保留 `error` 兼容字段) | | `common.moderation.AiContentFilterGuard` | 串联「主动检测 → 弹性调用 → 拦截响应」 | | `common.moderation.AiContentFilterService` | 输入/输出内容检测 | | `common.moderation.ContentFilterMatcher` | 敏感词 + 正则匹配 | | `common.resilience.AiCallResilienceExecutor` | 限流 + 按 provider 熔断 + 自动重试 | | `common.resilience.AiProviderRateLimiter` | 基于 Resilience4j `RateLimiter`,按 provider 客户端控速 | | `common.resilience.AiRetryableClassifier` | 判断是否应用层重试 / 是否计入熔断失败 | | `common.resilience.AiRateLimitRetryDelay` | 429 延迟重试间隔(Retry-After 优先) | | `common.resilience.RateLimitSource` | 限流来源枚举:`LOCAL` / `PROVIDER` | | `common.config.LlmResilienceProperties` | `app.llm.resilience` 配置 | | `common.config.AiResilienceConfig` | `CircuitBreakerRegistry` + `RateLimiterRegistry` + Spring AI `RetryTemplate` | | `common.config.LlmContentFilterProperties` | `app.llm.content-filter` 配置 | > 已移除模块级 `LlmExceptionHandler` / `RagExceptionHandler`,统一由 `GlobalAiExceptionHandler` 处理。 #### 错误码一览 | errorCode | HTTP | 场景 | retryable | |---|---|---|---| | `AI_MODEL_TIMEOUT` | 504 | 连接/读取超时、408/504 | true | | `AI_RATE_LIMIT` | 429 | 本地控速超限或上游 429 / 频率过高 | true(配额用尽为 false) | | `AI_CONTENT_FILTER` | 422 | 内容安全/合规审核未通过 | false | | `AI_PROVIDER_UNAVAILABLE` | 503 | 502/503、Transient 暂态错误 | true | | `AI_CIRCUIT_OPEN` | 503 | 按 provider 熔断器 OPEN | true | | `AI_MODEL_ERROR` | 502 | 其他 NonTransient 模型错误 | false | | `AI_UNKNOWN` | 502 | 未分类的模型调用错误 | false | | `INVALID_ARGUMENT` / `ILLEGAL_STATE` | 400 | 参数/状态校验 | false | `AiExceptionTranslator` 分类依据:**HTTP 状态码** + **Spring AI 异常类型**(`TransientAiException` / `NonTransientAiException` / `OpenAiApiClientErrorException` / `DashScopeException`)+ **错误消息关键词**。 #### 接入点 | 模块 | 类 | 说明 | |---|---|---| | 统一对话 | `LlmChatService` | 同步 `guardChat`;流式 `guardStream` | | DeepSeek 专用 | `DeepSeekChatService` | 同上 | | RAG 问答 | `RagGenerator` | 检测用户 `question`,拦截生成答案 | | 向量检索 | `VectorizationService.embedQuery` | 检索前检测查询文本(仅 INPUT) | #### 错误响应格式 **上游限流示例(`rateLimitSource=PROVIDER`):** ```json { "error": "模型调用被限流,请稍后重试", "errorCode": "AI_RATE_LIMIT", "message": "模型调用被限流,请稍后重试", "provider": "deepseek", "retryable": true, "retryAfterSeconds": 30, "rateLimitSource": "PROVIDER" } ``` **本地控速超限示例(`rateLimitSource=LOCAL`):** ```json { "error": "本地请求速率超限(每 10 秒最多 3 次),请稍后重试", "errorCode": "AI_RATE_LIMIT", "message": "本地请求速率超限(每 10 秒最多 3 次),请稍后重试", "provider": "deepseek", "retryable": true, "retryAfterSeconds": 10, "rateLimitSource": "LOCAL" } ``` **内容过滤示例(主动检测拦截输入):** ```json { "error": "输入内容未通过安全审核,请修改后重试", "errorCode": "AI_CONTENT_FILTER", "message": "输入内容未通过安全审核,请修改后重试", "provider": "deepseek", "retryable": false, "retryAfterSeconds": null, "filterPhase": "INPUT", "filterSource": "LOCAL" } ``` | 字段 | 说明 | |---|---| | `error` | 与旧版 `Map.of("error", …)` 兼容 | | `errorCode` | 机器可读错误码 | | `retryable` | 是否建议客户端重试 | | `retryAfterSeconds` | 建议等待秒数(来自 `Retry-After` 头或本地窗口估算,可为 null) | | `rateLimitSource` | 限流来源:`LOCAL`(Resilience4j 客户端控速)/ `PROVIDER`(上游 429),仅 `AI_RATE_LIMIT` | | `filterPhase` | 内容过滤阶段:`INPUT` / `OUTPUT`,仅 `AI_CONTENT_FILTER` | | `filterSource` | 过滤来源:`LOCAL` / `PROVIDER`,仅 `AI_CONTENT_FILTER` | **流式接口**(`/api/llm/chat/stream`、`/api/deepseek/chat/stream`):出错时在 SSE 流末尾输出一行 JSON(结构同上,由 `GlobalAiExceptionHandler.toStreamErrorPayload()` 生成)。 --- #### 内容过滤策略:主动检测 + 拦截响应 在统一异常体系中,内容过滤采用 **三层防护**,避免违规内容进入模型或返回给用户: ``` 用户输入 ──► [1] 主动检测 INPUT ──► 弹性调用 ──► [2] 拦截响应 OUTPUT ──► 返回客户端 │ │ │ LOCAL │ LOCAL ▼ ▼ 不发起 API 调用 丢弃回复并抛错 模型调用过程中 ──► [3] 上游拒绝 PROVIDER ──► AiExceptionTranslator 翻译 ``` | 阶段 | 时机 | 组件 | 行为 | |---|---|---|---| | **主动检测** | 模型调用**前** | `AiContentFilterService.checkInput` | 关键词 + 正则匹配;不合规直接抛 `AiContentFilterException`,**不发起 API 调用** | | **拦截响应** | 模型返回**后** | `AiContentFilterService.checkOutput` | 校验完整回复;不合规则**丢弃**并抛错,不返回给用户 | | **上游拒绝** | 平台返回错误 | `AiExceptionTranslator` | 识别 content_policy / moderation 等关键词,翻译为 `filterSource=PROVIDER` | **流式对话说明**:启用输出检测时,会先 **缓冲完整回复**,通过 `checkOutput` 后再逐 chunk 下发(保证拦截生效)。 **配置**(`app.llm.content-filter`): ```yaml app: llm: content-filter: enabled: true # 总开关 check-input: true # 主动检测(调用前) check-output: true # 拦截响应(返回前) input-reject-message: 输入内容未通过安全审核,请修改后重试 output-reject-message: 模型回复未通过安全审核,已被拦截 blocked-keywords: # 敏感词(大小写不敏感,子串匹配) - 袭击 blocked-patterns: # 敏感正则(忽略大小写) - (?i)how\s+to\s+make\s+a\s+bomb ``` 可按业务扩展 `blocked-keywords` / `blocked-patterns`;仅需输入侧检测时可设 `check-output: false`。 **内容过滤策略扩展(用于生产环境)**: - 第 1 层(已支撑):本地关键词/正则 —— 快速挡明显违规,全 provider 可用 - 第 2 层(可追加):OpenAiModerationModel —— 本地通过后,再做语义审核 - 第 3 层(已支撑):Chat 调用 + 上游拒绝翻译 —— 平台侧最后一道关 #### 弹性策略:限流 + 熔断 + 自动重试 `AiCallResilienceExecutor` 对 **超时**、**网络异常**、**暂态不可用**、**上游 429** 等可恢复错误,提供 **按 provider 隔离** 的弹性策略: ``` 模型调用 → [1] AiProviderRateLimiter(Resilience4j RateLimiter,按 provider) → [2] CircuitBreaker(按 provider) → [3] 自动重试 executeWithRetry(指数退避,最多 max-attempts 次) → ChatClient / EmbeddingModel → 重试仍失败 → 计入熔断滑动窗口(部分异常除外,见下表) → 失败率超阈值 → OPEN(拒绝调用,errorCode=AI_CIRCUIT_OPEN) → 等待 wait-duration → HALF_OPEN 探测 → CLOSED 恢复 ``` | 策略 | 配置项 | 说明 | |---|---|---| | **客户端限流** | `app.llm.resilience.rate-limit.*` | Resilience4j `RateLimiter`;`acquirePermission()` 申请 **1 个许可**(参数为许可数,非超时毫秒) | | **自动重试** | `app.llm.resilience.retry.*` | 对 `AI_MODEL_TIMEOUT` / `AI_PROVIDER_UNAVAILABLE` / 上游 `AI_RATE_LIMIT`(`PROVIDER`)重试 | | **不重试** | — | `rateLimitSource=LOCAL` 的本地控速超限由 `AiRetryableClassifier` 排除,直接返回 429 | | **熔断** | `app.llm.resilience.circuit-breaker.*` | 内容过滤、本地限流、可恢复上游 429 **不计入**熔断统计 | | **HTTP 层** | `RetryTemplate` Bean | `resilience.enabled=true` 时 ChatModel 降为 1 次,避免与应用层双重重试 | **Resilience4j 配置映射**(`AiResilienceConfig.aiRateLimiterRegistry`): | `app.llm.resilience.rate-limit` | Resilience4j `RateLimiterConfig` | |---|---| | `limit-for-period` | `limitForPeriod` | | `limit-refresh-period-ms` | `limitRefreshPeriod` | | `acquire-timeout-ms` | `timeoutDuration`(0 = 超限立即拒绝;>0 = 在窗口内排队等待) | ```yaml app: llm: resilience: enabled: true retry: #重试配置 max-attempts: 3 # 最多尝试 3 次(含首次) initial-interval-ms: 1000 # 初始延迟 1 秒 multiplier: 2.0 # 指数退避倍数 2x max-interval-ms: 10000 # 最大延迟 10 秒 rate-limit-initial-interval-ms: 2000 # 限流专用初始延迟 2 秒 rate-limit-multiplier: 2.0 # 限流退避倍数 honor-retry-after-header: true # 优先使用上游 Retry-After 响应头 rate-limit: #限流配置 enabled: true limit-for-period: 3 # 每窗口最多 3 次(测试可改小;生产建议 20+) limit-refresh-period-ms: 10000 # 窗口每10秒重置令牌桶,补充3个令牌 acquire-timeout-ms: 0 # 0=超限立即拒绝,不排队等待 circuit-breaker: # 熔断器配置 failure-rate-threshold: 50 # 失败率阈值 50% sliding-window-size: 10 # 滑动窗口大小 10 次请求 minimum-number-of-calls: 5 # 最小调用数 5 次后才开始计算 wait-duration-in-open-state-ms: 30000 # OPEN 状态持续 30 秒 permitted-number-of-calls-in-half-open-state: 3 # HALF_OPEN 允许 3 次探测 ``` #### HTTP 429 限流:本地控速 + 上游延迟重试 针对限流场景,采用「**本地主动控速** + **上游 429 延迟重试**」双层策略: ``` 请求进入 → [1] AiProviderRateLimiter rateLimiterRegistry.rateLimiter(provider) rateLimiter.acquirePermission() # 申请 1 个许可 超限 → AI_RATE_LIMIT + rateLimitSource=LOCAL(不重试) → [2] 模型调用 → 若上游 429 → AiExceptionTranslator → AI_RATE_LIMIT + rateLimitSource=PROVIDER → AiRateLimitRetryDelay:优先 Retry-After,否则 rate-limit-initial-interval-ms 指数退避 → 延迟后自动重试(最多 max-attempts 次) → 可恢复的上游 429 **不计入**熔断器失败统计 ``` | 组件 | 职责 | |---|---| | `AiResilienceConfig.aiRateLimiterRegistry` | 注册 Resilience4j `RateLimiterRegistry`,按 provider 懒创建实例 | | `AiProviderRateLimiter` | 调用前获取许可,降低触发上游 429 的概率 | | `AiExceptionTranslator` | 识别 HTTP 429 / 限流关键词,解析 `Retry-After` 写入 `retryAfterSeconds` | | `AiRateLimitRetryDelay` | 上游 429 重试间隔:优先 `Retry-After`,否则限流专用指数退避 | | `AiRetryableClassifier` | `LOCAL` 限流不重试;本地限流与可恢复上游 429 不计入熔断 | --- ## 快速开始 ### RAG ETL 流水线详解 RAG ETL(Extract-Transform-Load)流水线负责将原始文档转换为可检索的向量数据,核心流程如下: #### 1. 整体架构 DocumentReader → DocumentTransformer → DocumentWriter [读取] [转换/增强] [写入/存储向量数据库(Vector Store)] ETL阶段 → 向量存储 → 检索阶段 → Prompt填充 → LLM生成 #### 2. 核心组件 **(1)Reader - 文档读取器** - **实现类**:`RagPdfPageDocumentReader`(PDF)、`RagWordDocumentReader`(Word)、`RagExcelDocumentReader`(Excel) - **职责**:从原始文件提取文本内容,按页或逻辑单元拆分为初始 Document 列表 - **输出**:每个 Document 代表一页(PDF)或一个工作表(Excel),包含原始文本和基础元数据 **(2)Transformer - 转换器链** 按顺序执行多个转换器,对文档进行分块和元数据增强: - **RagTokenSplitTransformer**(分块转换器) - 基于 Spring AI 的 `TokenTextSplitter` 按 Token 粒度智能分块 - 配置参数:`chunkTokenSize`(分块大小,默认 512)、`chunkOverlap`(重叠比例,默认 50) - 标点感知:优先在句子边界(。!?;?\n)处切分,保持语义完整性 - 输出:1 个文档可能产生 N 个分块(取决于文本长度) - **RagChunkMetadataTransformer**(元数据增强器) - 为每个分块注入业务元数据: - `knowledgeBaseId`:知识库 ID(多租户隔离) - `documentId`:文档 ID(支持按文档启用/禁用) - `fileName`:原始文件名(前端展示来源) - `fileType`:文件类型(pdf/docx/xlsx) - `chunkIndex`:分块索引(定位原文位置) - `pipeline`:流水线类型标记("pdf-etl") - 保留原始元数据,避免丢失前序阶段的信息 **(3)Writer - 向量写入器** - **实现类**:`RagPgVectorDocumentWriter` - **职责**: 1. 根据知识库配置动态创建 Embedding 模型(openai / dashscope / ollama) 2. 批量调用 Embedding API 生成分块向量(DashScope 限制每批最多 10 条) 3. 将向量数据批量 upsert 到 pgvector 向量表 - **分批策略**:自动将大量分块按批次处理,避免触发 API 限制 #### 3. 处理流程示例(PDF 文档) 以 PDF 文档为例,完整 ETL 流程图: ![img.png](img.png) ### 环境要求 - JDK 21+ - Maven 3.8+ - MySQL、Redis(与 `application.yml` 中地址一致) - **PostgreSQL(已安装 pgvector 扩展)**(RAG 向量库) - 至少一个 LLM 提供商的有效 API Key(或本地 Ollama) - **RAG 嵌入模型 Key**:知识库创建时选择 `openai` / `dashscope` / `ollama`(DeepSeek 不支持嵌入) - 使用管理前台时:Node.js 18+、npm ### 配置 API Key 推荐通过环境变量注入(勿将真实 Key 提交到 Git): ```powershell # Windows PowerShell $env:DEEPSEEK_API_KEY="your-deepseek-key" $env:DASHSCOPE_API_KEY="your-dashscope-key" $env:OPENAI_API_KEY="your-openai-key" ``` 或在 `application.yml` 的 `spring.ai.provider.providers..api-key` 中使用占位符: ```yaml api-key: ${DEEPSEEK_API_KEY:} ``` ### 初始化 RAG 数据库 **PostgreSQL**(元数据表,在业务库执行): ```bash # 脚本路径:src/main/resources/db/rag-schema.sql ``` **PostgreSQL**(向量库,Docker 示例): ```bash docker run -it --rm --name pgvector -p 5432:5432 -e POSTGRES_PASSWORD=postgres pgvector/pgvector ``` 在 `application.yml` 配置 `app.rag.pg.*`;`app.rag.vector.initialize-schema: true` 时应用启动会自动建向量表。 ### 构建与运行 ```bash mvn -q -DskipTests package java -jar target/app.jar ``` 开发时也可: ```bash mvn spring-boot:run ``` 默认端口:**8080**。Knife4j 文档:启动后访问 `/doc.html`(若已启用)。 ### 启动管理前台(可选) ```bash cd portalUI npm install npm run dev ``` 浏览器访问 **http://localhost:5173**。默认通过 `.env.development` 直连 `http://localhost:8080/api`(后端 `WebCorsConfig` 已放行跨域)。生产构建:`npm run build`,产物在 `portalUI/dist/`。 技术栈、目录结构与各模块职责见 **[portalUI/README.md](portalUI/README.md)**。 ## 配置说明 多模型声明集中在 `spring.ai.provider`;对话记忆在 `app.llm.memory`;RAG 在 `app.rag`: ```yaml spring: ai: chat: client: enabled: false memory: repository: jdbc: initialize-schema: always # 自动建 SPRING_AI_CHAT_MEMORY 表 provider: default: deepseek routing: deepseek-chat: deepseek qwen-plus: dashscope providers: deepseek: type: deepseek enabled: true display-name: DeepSeek api-key: ${DEEPSEEK_API_KEY:} model: deepseek-chat temperature: 0.8 dashscope: type: dashscope enabled: true display-name: 通义千问 api-key: ${DASHSCOPE_API_KEY:} model: qwen-plus vectorstore: pgvector: enabled: false # 使用 app.rag.pg 独立数据源,排除自动配置 app: llm: memory: enabled: true max-messages: 20 #配置滑动窗口最大消息条数 prompt: enabled: true system-template: classpath:prompts/llm/default-system.st user-template: classpath:prompts/llm/default-user.st wrap-user-input: false content-filter: enabled: true check-input: true check-output: true input-reject-message: 输入内容未通过安全审核,请修改后重试 output-reject-message: 模型回复未通过安全审核,已被拦截 blocked-keywords: - 袭击 blocked-patterns: - (?i)how\s+to\s+make\s+a\s+bomb - (?i)bypass\s+content\s+filter resilience: enabled: true retry: #重试配置 max-attempts: 3 # 最多尝试 3 次(含首次) initial-interval-ms: 1000 # 初始延迟 1 秒 multiplier: 2.0 # 指数退避倍数 2x max-interval-ms: 10000 # 最大延迟 10 秒 rate-limit-initial-interval-ms: 2000 # 限流专用初始延迟 2 秒 rate-limit-multiplier: 2.0 # 限流退避倍数 honor-retry-after-header: true # 优先使用上游 Retry-After 响应头 rate-limit: #限流配置 enabled: true limit-for-period: 3 # 每窗口最多 3 次(测试可改小;生产建议 20+) limit-refresh-period-ms: 10000 # 窗口每10秒重置令牌桶,补充3个令牌 acquire-timeout-ms: 0 # 0=超限立即拒绝,不排队等待 circuit-breaker: # 熔断器配置 failure-rate-threshold: 50 # 失败率阈值 50% sliding-window-size: 10 # 滑动窗口大小 10 次请求 minimum-number-of-calls: 5 # 最小调用数 5 次后才开始计算 wait-duration-in-open-state-ms: 30000 # OPEN 状态持续 30 秒 permitted-number-of-calls-in-half-open-state: 3 # HALF_OPEN 允许 3 次探测 rag: enabled: true storage-path: ./data/rag default-top-k: 5 default-similarity-threshold: 0.6 prompt: system-template: classpath:prompts/rag/default-system.st user-template: classpath:prompts/rag/default-user.st pg: url: jdbc:postgresql://127.0.0.1:5432/postgres username: postgres password: postgres vector: table-name: rag_vector_store dimensions: 1024 # 须与嵌入模型维度一致 initialize-schema: true index-type: HNSW distance-type: COSINE_DISTANCE ``` | 字段 | 说明 | |---|---| | `default` | 默认 provider id | | `routing` | model 名称 → provider id | | `providers..enabled` | 是否启用;`false` 时不创建客户端 | | `providers..type` | 底层类型:`deepseek` / `openai` / `ollama` / `dashscope` | | `providers..display-name` | 展示名称(出现在 `/api/llm/providers`) | | `providers..description` | 提供商描述 | | `providers..api-key` | API Key(Ollama 可省略,需 `base-url`) | | `providers..model` | 默认模型名 | | `providers..models` | 额外可路由的模型别名(列表) | | `app.llm.memory.enabled` | 是否启用 JDBC 对话记忆 | | `app.llm.memory.max-messages` | 滑动窗口最大消息条数 | | `app.llm.prompt.enabled` | 是否启用 LLM Prompt 模板化组装 | | `app.llm.prompt.system-template` | 通用对话 system 模板(classpath 或内联) | | `app.llm.prompt.wrap-user-input` | 是否用 user 模板包装用户输入 | | `app.llm.content-filter.enabled` | 是否启用本地内容过滤 | | `app.llm.content-filter.check-input` | 模型调用前主动检测输入 | | `app.llm.content-filter.check-output` | 模型返回后拦截不合规输出 | | `app.llm.content-filter.blocked-keywords` | 敏感词列表(子串匹配) | | `app.llm.content-filter.blocked-patterns` | 敏感正则列表 | | `app.llm.resilience.enabled` | 应用层重试 + 熔断总开关 | | `app.llm.resilience.retry.max-attempts` | 最大尝试次数(含首次) | | `app.llm.resilience.circuit-breaker.failure-rate-threshold` | 熔断失败率阈值(%) | | `app.llm.resilience.circuit-breaker.wait-duration-in-open-state-ms` | OPEN 状态持续毫秒数 | | `app.rag.enabled` | RAG 模块开关 | | `app.rag.prompt.system-template` | RAG 问答 system 模板 | | `app.rag.prompt.user-template` | RAG 问答 user 模板(含 `{context}`、`{question}`) | | `app.rag.storage-path` | 上传文档本地存储目录 | | `app.rag.pg.*` | PostgreSQL 连接(pgvector) | | `app.rag.vector.dimensions` | 向量维度,须与嵌入模型一致 | | `app.rag.vector.initialize-schema` | 是否自动创建向量表 | 扩展 OpenAI 兼容网关时,可在对应 provider 上配置 `base-url`(如 Moonshot、智谱),并在 `AiProviderChatModelFactory` 中按 `type: openai` 走 OpenAI 客户端。 ## API 示例 ### LLM 对话 ```bash # 查看配置与可用提供商 curl http://localhost:8080/api/llm/config curl http://localhost:8080/api/llm/providers # 按 model 解析路由 curl "http://localhost:8080/api/llm/route?model=qwen-plus" # 对话(使用默认 provider) curl "http://localhost:8080/api/llm/chat?message=你好" # 指定 provider curl "http://localhost:8080/api/llm/chat?provider=deepseek&message=你好" # 仅传 model,自动路由 curl "http://localhost:8080/api/llm/chat?model=qwen-plus&message=你好" # 多轮对话(同一 conversationId 共享上下文) curl "http://localhost:8080/api/llm/chat?message=我叫小明&conversationId=session-001" curl "http://localhost:8080/api/llm/chat?message=我叫什么&conversationId=session-001" # 工具调用示例(同步对话,LLM 按需检索工具) curl "http://localhost:8080/api/llm/chat?message=武汉今天天气怎么样" curl "http://localhost:8080/api/llm/chat?message=查一下订单ORD12345678的状态" # 运行时切换默认 provider curl -X POST "http://localhost:8080/api/llm/switch?provider=dashscope" # 恢复默认 provider curl -X POST "http://localhost:8080/api/llm/default/reset" # DeepSeek 专用接口 curl "http://localhost:8080/api/deepseek/chat?message=你好" ``` ### Agent 编排(curl 速查,完整设计见上文) ```bash # P0/P1 同步 curl -X POST http://localhost:8080/api/agent/run \ -H "Content-Type: application/json" \ -d "{\"goal\":\"查一下订单ORD12345678的状态,并用一句话总结\",\"maxSteps\":5}" # P1 带知识库(RAG Skill) curl -X POST http://localhost:8080/api/agent/run \ -H "Content-Type: application/json" \ -d "{\"goal\":\"结合知识库说明退货政策,并查订单ORD12345678状态\",\"knowledgeBaseId\":1,\"maxSteps\":6}" # P2 异步 + 计划审批 + 并行 curl -X POST http://localhost:8080/api/agent/run/async \ -H "Content-Type: application/json" \ -d "{\"goal\":\"查订单ORD12345678并总结\",\"requirePlanApproval\":true,\"parallelSteps\":true}" # P2 SSE(另开终端) curl -N http://localhost:8080/api/agent/tasks/{taskId}/stream # P2 人工批准 curl -X POST http://localhost:8080/api/agent/tasks/{taskId}/resume \ -H "Content-Type: application/json" \ -d "{\"approved\":true,\"comment\":\"ok\"}" # 查询落库轨迹 / 近期任务 curl http://localhost:8080/api/agent/tasks/{taskId} curl "http://localhost:8080/api/agent/tasks?limit=20" curl http://localhost:8080/api/agent/specialists curl "http://localhost:8080/api/agent/dashboard/summary?limit=50" ``` ### RAG 知识库与问答 ```bash # 创建知识库 curl -X POST http://localhost:8080/api/rag/knowledge-bases \ -H "Content-Type: application/json" \ -d '{"name":"产品手册","description":"产品文档","embeddingProvider":"dashscope","embeddingModel":"text-embedding-v3","parseMethod":"AUTO","chunkTokenSize":512,"chunkOverlap":50,"systemPrompt":"依据上下文回答"}' # 知识库列表 curl http://localhost:8080/api/rag/knowledge-bases # 上传文档(multipart) curl -X POST "http://localhost:8080/api/rag/knowledge-bases/1/documents/upload" \ -F "file=@./sample.pdf" # 文档列表 curl http://localhost:8080/api/rag/knowledge-bases/1/documents # 解析并向量化 curl -X POST http://localhost:8080/api/rag/knowledge-bases/1/documents/1/parse # 相似度检索 curl "http://localhost:8080/api/rag/search?knowledgeBaseId=1&query=保修期&topK=5" # RAG 问答 curl -X POST http://localhost:8080/api/rag/ask \ -H "Content-Type: application/json" \ -d '{"knowledgeBaseId":1,"question":"产品保修期多久?","topK":5,"provider":"deepseek","model":"deepseek-chat"}' # RAG 多轮 Chat(同一 conversationId 共享上下文) curl -X POST http://localhost:8080/api/rag/chat \ -H "Content-Type: application/json" \ -d '{"knowledgeBaseId":1,"question":"产品保修期多久?","topK":5,"provider":"deepseek","model":"deepseek-chat","conversationId":"chat-001"}' curl -X DELETE http://localhost:8080/api/rag/chat/chat-001 # 实时文本向量化 curl -X POST http://localhost:8080/api/rag/vectorize/realtime \ -H "Content-Type: application/json" \ -d '{"knowledgeBaseId":1,"texts":["第一条知识","第二条知识"]}' ``` 完整路径列表见 [AGENTS.md#现有-api-速查](AGENTS.md#现有-api-速查)。 ## 管理前台(portalUI) | 类别 | 技术 | |------|------| | 框架 / 构建 | Vue 3.5、Vite 6、TypeScript 5.7 | | UI / 路由 | Element Plus 2.9、Vue Router 4.5、@element-plus/icons-vue | | HTTP | Axios 1.7(`src/api/request.ts` 统一封装) | | 路由 | 页面 | 说明 | |------|------|-----------------------------------| | `/knowledge-bases` | 知识库列表与创建 | 对接知识库 CRUD API | | `/knowledge-bases/:id` | 文档管理 | 上传、解析、向量化、启用/删除 | | `/vectorize` | 向量化服务 | 文档同步/异步、实时文本向量化 | | `/search` | 相似度检索 | pgvector 召回(GET/POST) | | `/ask` | RAG 问答 | 检索 + LLM 生成,展示引用来源 | | `/chat` | RAG Chat | 上/下/右布局多轮对话;右侧配置同 RAG 问答 或普通LLM对话 | | `/agent` | Agent 任务 | 异步提交、SSE 轨迹、人工审批、metrics / Specialist(P2/P3) | | `/agent/dashboard` | Agent 仪表盘 | 成本 / 步数 / Token 聚合(P3) | | `/agent/eval` | Agent 评估 | 轨迹 Eval 测试集与 Run(P3) | ``` portalUI/src/ ├── api/ # REST 封装(knowledgeBase、document、vectorize、rag、llm) ├── types/ # TS 类型,对齐 Java ragapi DTO ├── views/ # 五个业务页面 ├── layouts/ # MainLayout 侧边栏布局 ├── router/ # 路由配置 ├── utils/ # 格式化工具 └── styles/ # 全局样式 ``` 完整技术栈、目录树与各目录职责说明见 **[portalUI/README.md](portalUI/README.md)**。 ## 项目结构 ``` SpringAIProject/ ├── portalUI/ # Vue 3 RAG 管理前台(详见 portalUI/README.md) │ ├── src/api/ # REST 封装 │ ├── src/views/ # 知识库、文档、向量化、检索、问答页面 │ ├── src/types/ # TS 类型(对齐 ragapi) │ ├── src/layouts/ # MainLayout │ └── package.json ├── src/main/java/com/springai/harrycj/ │ ├── AppAiMain.java │ ├── base/controller/ │ ├── business/ │ │ ├── common/ # 示例 CRUD │ │ ├── deepseek/ # DeepSeek 专用 API │ │ ├── llm/ # 多模型统一 API │ │ │ ├── controller/ │ │ │ ├── service/ │ │ │ ├── tool/ │ │ │ └── vo/ │ │ ├── agent/ # Agent 编排(Planner / Executor / Specialist / Eval / Dashboard) │ │ │ ├── controller/ │ │ │ ├── service/ │ │ │ ├── planner/ │ │ │ ├── executor/ │ │ │ ├── context/ │ │ │ ├── model/ │ │ │ └── agentapi/ │ │ └── rag/ # RAG 模块 │ │ ├── ragapi/ # DTO │ │ ├── context/ # RAG Context 层(RagAskContextBuilder、RagPromptPipeline) │ │ ├── controller/ │ │ ├── service/ │ │ ├── retriever/ │ │ ├── generator/ │ │ ├── parser/ │ │ ├── vector/ │ │ ├── entity/ │ │ └── mapper/ │ └── common/ │ ├── config/ # 多模型、记忆、Prompt、工具、Agent、RAG、CORS、内容过滤配置 │ ├── context/ # Context 层(LlmChatContextBuilder、LlmPromptPipeline) │ ├── prompt/ # Prompt 层(PromptContextAssembler、AiPromptTemplateService) │ ├── harness/ # Harness 层(AiChatHarness、HarnessProfile、HarnessRequest) │ ├── exception/ # 统一 AI 异常(AiExceptionTranslator、GlobalAiExceptionHandler) │ ├── moderation/ # 内容过滤(AiContentFilterGuard、AiContentFilterService) │ └── resilience/ # 弹性调用(AiCallResilienceExecutor、AiProviderRateLimiter、AiRetryableClassifier) └── src/main/resources/ ├── application.yml ├── prompts/ # Prompt 模板(llm/、rag/、agent/) ├── db/rag-schema.sql # RAG PostgreSQL 建表脚本 ├── logback-spring.xml └── mapper/ ``` ## 常见问题 **启动报错 `DeepSeek API key must be set`** - 原因:Starter 自动装配与手动工厂冲突,或 Key 未配置。 - 处理:确保 `spring.ai.provider.providers..api-key` 有效;将未使用的 `spring.ai.deepseek/dashscope/openai` 等设为 `enabled: false`;仅启用已配置 Key 的 provider。 **调用返回「未就绪」或 400** - 检查对应 provider 的 `enabled` 与 API Key;调用 `GET /api/llm/providers` 查看 `available` 状态。 **多轮对话无上下文** - 确认多轮请求使用同一 `conversationId`。 - 检查 `app.llm.memory.enabled` 是否为 `true`,MySQL 中 `SPRING_AI_CHAT_MEMORY` 表是否已创建。 **工具调用无响应** - 动态工具发现仅在同步 `GET /api/llm/chat` 生效,流式接口不含工具检索。 - 响应中 `toolsEnabled` 为 `true` 表示工具已注册。 **RAG 启动失败或向量写入报错** - 确认 PostgreSQL 已启动且已安装 `vector` 扩展;检查 `app.rag.pg.*` 连接配置。 - `app.rag.vector.dimensions` 须与嵌入模型输出维度一致(如 DashScope `text-embedding-v3` 常用 1024)。 - PostgreSQL 需先执行 `src/main/resources/db/rag-schema.sql`。 **RAG 嵌入模型报错** - DeepSeek 不支持嵌入;知识库请选择 `openai` / `dashscope` / `ollama` 并配置对应 API Key。 **前台请求不到后端** - 开发环境默认 `VITE_API_BASE_URL=http://localhost:8080/api`(见 `portalUI/.env.development`),需后端已启动且 `WebCorsConfig` 生效。 - 若改用 Vite 代理,将 `VITE_API_BASE_URL` 设为 `/api` 并重启 `npm run dev`。 - `npm run preview` 生产预览同样支持代理;直接打开 `dist/index.html` 无效。 **模型调用返回 429 / 504 / 422** - 响应体 `errorCode` 分别为 `AI_RATE_LIMIT`、`AI_MODEL_TIMEOUT`、`AI_CONTENT_FILTER`;`retryable=true` 时可稍后重试。 - `AI_RATE_LIMIT` 时查看 `rateLimitSource`:`LOCAL` 为 Resilience4j 客户端控速(不重试),`PROVIDER` 为上游 429(应用层延迟重试)。 - 应用层已启用 `AiProviderRateLimiter` 主动控速;上游 429 按 `Retry-After` 或 `rate-limit-initial-interval-ms` 延迟重试,详见上文「HTTP 429 限流:本地控速 + 上游延迟重试」。 - 流式接口错误以 SSE 流内 JSON 单行返回,前端需解析 `errorCode` 字段。 **内容过滤返回 422(`AI_CONTENT_FILTER`)** - `filterSource=LOCAL`:本地主动检测(`INPUT`)或拦截响应(`OUTPUT`),可在 `app.llm.content-filter` 调整敏感词/正则。 - `filterSource=PROVIDER`:上游模型平台拒绝(如 DashScope `data_inspection_failed`、OpenAI `content_policy_violation`)。 - 主动检测在调用前拦截,不会消耗模型 API 配额;仅需输入侧检测时可设 `check-output: false`。 - 流式对话启用输出检测时会缓冲完整回复后再下发,详见上文「内容过滤策略:主动检测 + 拦截响应」。 **模型服务熔断(503,`AI_CIRCUIT_OPEN`)** - 某 provider 连续超时/不可用导致熔断器 OPEN 时,该 provider 请求会被快速失败,避免雪崩。 - 等待 `wait-duration-in-open-state-ms` 后进入 HALF_OPEN 探测;可调低 `failure-rate-threshold` 或 `sliding-window-size` 提高灵敏度。 - 设置 `app.llm.resilience.enabled=false` 可关闭应用层重试/熔断,改由 Spring AI `RetryTemplate` 在 HTTP 层重试。 ## 参与贡献 1. Fork 本仓库 2. 新建功能分支 3. 提交变更并发起 Pull Request 变更涉及依赖、LLM 接入或 RAG/前台时,请同步更新 `pom.xml`、`AGENTS.md`、`portalUI/` 与本 `README.md`。 ## 许可证 见仓库根目录许可文件(如有)。