# 普通聊天 + 任务流水线(意图路由)设计方案 > 状态:设计稿,未实施 > 日期:2026-10-20 > 范围:`ai-server/src/main/java/com/zsjz/ai/module/agent/`(意图识别、闲聊/任务流水线) > 关联需求:在现有「数据分析」AI 流水线之外,新增一条同时承接**普通聊天**与**任务执行**的流水线,由意图识别按条消息自动路由。 --- ## 0. 需求口径 用户原话中的「人物」按同音词「任务」理解(已确认:"任务。")。要新增**一条** AI 流水线,同时承接: - **普通聊天**:问候、感谢、能力询问、与案件数据无关的一般问答; - **任务执行**:工作区文件读写/检索、执行命令、执行 Python 脚本、批量文件处理等。 该流水线与现有数据分析流水线并存,按意图**逐条消息自动路由**。分类失败、超时、未知意图一律回落到**数据分析流水线**(保持现状,fail-safe)。现有数据分析 agent 的 67 个业务工具、提示词、SSE 契约全部不动。 --- ## 1. 现状结论(决定了改法) - `IntentMiddleware` 只做三件事:调 LLM 分类、往输入追加 `` 合成消息、发 `intent` SSE 帧;**不改变执行路径**。所有意图都走同一个 `HarnessAgent`、同一套工具。 - `AgentChatServiceImpl#doStreamLocked` 是唯一调用 `AgentService.getOrCreateAgent` 的地方,路由改造点单一。 - `IntentEnum` 目前是死代码(`fromCode` 无调用方),`IntentResult` 注释、`prompts/intent-recognition.txt` 里还留着旧的二分类 `《闲聊或无关指令》/《可能的数据分析请求》` 和已不存在的 `response` 字段。 - `IntentProperties.timeoutSeconds/maxTokens` 从未生效;`IntentService.safeAnalyze` 的 `.block()` 没有超时,模型卡住会阻塞整条流。 - AgentScope 2.0.1 Harness 自带以下工具(已从 jar 核实): - 文件:`read_file`、`write_file`、`edit_file`、`grep_files`、`glob_files`、`list_files`; - 命令:`execute`(仅当文件系统是 `AbstractSandboxFilesystem` 时注册,`LocalFilesystemSpec` 满足); - 构建器开关:`disableShellTool()`、`disableMemoryTools()`、`enableTaskList(boolean)`、`toolsConfig(...)`; - `LocalFilesystemSpec`:`project(Path)`(同时设置 shell cwd 与路径策略根)、`mode(LocalFsMode)`、`addRoot(Path)`、`executeTimeoutSeconds(int)`、`maxOutputBytes(int)`、`env(...)`、`inheritEnv(boolean)`。 - 现有数据分析 agent 也挂了 `LocalFilesystemSpec`,所以它本来就有 shell/文件工具。新流水线的价值是:**去掉 55 个业务工具 + 数据 render 契约,换成任务型提示词与更严格的路径边界**。 - 已有独立流水线的成熟先例是 AI 研判(`InsightController` + `InsightAgentFactory` + 静态 prompt),但它按 endpoint 隔离,不是按意图。 --- ## 2. 总体架构 ``` POST /chat/stream ├─ 会话/附件/模型解析(沿用现有逻辑) ├─ ChatIntentRouter.route(modelId, sessionId, question) │ └─ IntentService.safeAnalyze → LlmService.chatAs(超时/关思考/fail-open) │ ├─ data_query / schema_explore / etl_clean / visualization / null │ │ → DataAnalysisPipeline(现有 HarnessAgent + AgentService 实例池) │ └─ task_execute / general_chat │ → TaskPipeline(TaskAgentFactory:文件/命令/Python + 任务提示词) ├─ 事件流头部注入 intent CustomEvent(code/label/pipeline/enhanced/entities) └─ 原 SSE 转换、工具事件聚合、assistant 落库、心跳、取消处理全部复用 ``` 两条流水线都返回 `Flux`,因此 `AgentChatServiceImpl` 里那一大段事件→SSE 的转换代码**一行不改**。 --- ## 3. 意图层改造 ### 3.1 `IntentEnum`(新增枚举值与流水线归属) ```java DATA_QUERY("data_query", "数据查询", DATA_ANALYSIS) SCHEMA_EXPLORE("schema_explore", "表结构探索", DATA_ANALYSIS) ETL_CLEAN("etl_clean", "数据处理", DATA_ANALYSIS) VISUALIZATION("visualization", "图表可视化", DATA_ANALYSIS) TASK_EXECUTE("task_execute", "任务执行", TASK) GENERAL_CHAT("general_chat", "通用问答", TASK) // 宽松归一:去《》/空白/大小写,先匹配 code 再匹配 label;未知返回 null。 // 不要回落 GENERAL_CHAT,否则未知意图会被误送任务流水线。 public static IntentEnum fromAny(String raw) ``` `IntentEnum.fromCode` 保留(兼容),新增 `fromAny` 供路由使用。 ### 3.2 `IntentResult` + 提示词 - `IntentResult.intent` 的语义改为 **code**(`data_query` 等),修掉过期的 `《...》` 注释。 - 重写 `prompts/intent-recognition.txt`: - 6 个标签,写清每条流水线的判定边界; - 明确规则:最新输入为主、历史只补指代;**拿不准 → `data_query`**;纯问候/能力询问 → `general_chat`;只有明确的文件/命令/脚本操作才 `task_execute`; - 删除 `《》` 强制要求、删除旧二分类段落和 `response` 字段; - 移除 `{format}` 占位(schema 交给 `LlmService.chatAs` 追加),只保留 `{multi_turn}`。 - 数据结构仍走 `@JsonPropertyDescription` 生成 schema。 ### 3.3 `IntentService` 改造 - 注入 `LlmService`,用 `chatAs(request, IntentResult.class)` 替换自建 `ReActAgent`: - `enableThinking(false)`(分类任务不需要思考,避免 reasoning 吃掉 token); - `maxTokens(props.maxTokens)`、`timeout(Duration.ofSeconds(props.timeoutSeconds))` —— 这两个配置终于生效; - 任何异常/解析失败 → `null`(fail-open)。 - 返回新 record `IntentDecision(IntentEnum intent, String enhanced, List entities)`;`fromAny` 为 null 时返回 null。 ### 3.4 `IntentMiddleware`(只保留为回滚路径) - 改为消费 `IntentDecision`,渲染时用 `label`,`intent` 帧仍下发中文 label(前端兼容)。 - `AgentService.buildAgent` 中**条件挂载**: ```java if (!etlProperties.getIntent().isRouteEnabled()) { b.middleware(new IntentMiddleware(intentService, etlProperties.getIntent(), modelId)); } ``` - 路由开启时中间件不挂载,避免每轮分类两次。 --- ## 4. 路由层 新增 `module/agent/intent/ChatIntentRouter.java`: ```java public record RouteResult(IntentDecision decision, ChatPipeline pipeline) {} public RouteResult route(Long modelId, Long sessionId, String question) { if (!intentEnabled || !routeEnabled) return new RouteResult(null, dataPipeline); IntentDecision d = intentService.safeAnalyze(modelId, sessionId, question); if (d == null) return new RouteResult(null, dataPipeline); ChatPipeline p = d.intent().pipeline() == PipelineType.TASK ? taskPipeline : dataPipeline; return new RouteResult(d, p); } ``` `intent` 帧由 `IntentFrames.of(decision)` 统一生成: ```json {"type":"intent","code":"task_execute","label":"任务执行","intent":"任务执行", "pipeline":"task","enhanced":"...","entities":[]} ``` 保留 `intent` = label 是前端兼容做法;新增 `code/label/pipeline` 供 UI 精确展示。 --- ## 5. 流水线抽象 新增 `module/agent/pipeline/`: ```java public interface ChatPipeline { PipelineType type(); // DATA_ANALYSIS / TASK Flux stream(ChatPipelineContext ctx); } public record ChatPipelineContext( Long sessionId, Long caseId, Long modelId, Long agentRowId, String userId, SkillFilter skillFilter, String userText, List messages, IntentDecision intent) {} ``` ### 5.1 `DataAnalysisPipeline` 把现有 `doStreamLocked` 里的 agent 获取 + `RuntimeContext` 构建搬进来,行为保持不变: ```java HarnessAgent agent = agentService.getOrCreateAgent(ctx.caseId(), ctx.agentRowId(), ctx.modelId()); RuntimeContext rc = RuntimeContext.builder() .userId(ctx.userId()) .sessionId(String.valueOf(ctx.sessionId())) .put(SkillFilter.class, ctx.skillFilter()) .build(); return agent.streamEvents(ctx.messages(), rc); ``` ### 5.2 `TaskPipeline` ```java HarnessAgent agent = taskAgentFactory.getOrCreate(ctx.userId(), ctx.caseId(), ctx.modelId()); RuntimeContext rc = RuntimeContext.builder() .userId(ctx.userId()) .sessionId(ctx.sessionId() + "-task") // 与数据流水线隔离记忆槽 .put(SkillFilter.class, ctx.skillFilter()) .build(); return agent.streamEvents(ctx.messages(), rc); ``` --- ## 6. 任务流水线(`module/agent/task/`) ### 6.1 `TaskAgentFactory` 沿用 `InsightAgentFactory` + `AgentInstancePool` 的模式: - 池 key:`task-u{userId}-w{caseId}-m{modelId}`;上限 32,空闲 30 分钟回收;构建在锁外。 - 工作区:`WorkspaceManagerFactory.resolveAgentDataPath(cwd, userId, caseId, 9528L)`(固定虚拟 agent id,避免和真实 `agent.row_id` 冲突)。 - 构建: ```java HarnessAgent.builder() .name("task-agent").description("通用聊天与任务执行助手") .sysPrompt(taskPromptBuilder.build(caseId)) .maxIters(props.getMaxIters()) // 默认 20 .model(model).agentId("task-agent") .workspace(workspace) .filesystem(new LocalFilesystemSpec() .isolationScope(IsolationScope.USER) .project(workspace) // shell cwd = 工作区;ROOTED 路径策略根 .mode(LocalFsMode.ROOTED) .executeTimeoutSeconds(props.getShellTimeoutSeconds()) .maxOutputBytes(props.getMaxOutputBytes()) .addRoot(attachmentRoot(userId, caseId))) // 允许读本案附件 .toolkit(new Toolkit()) // 空业务工具 .enableMetaTool(false) // 不要 reset_equipped_tools .enableTaskList(true) // todo_write,多步任务可规划 .disableMemoryTools() // 去掉 memory_*/session_*,避免与后缀 sessionId 冲突 .stateStore(agentStateStore); if (!props.isShellEnabled()) builder.disableShellTool(); if (props.isSkillsEnabled() && 用户技能目录存在) builder.skillRepository(...); ``` ### 6.2 工具清单(全部是 Harness 内置,无需新写 `@Tool`) | 工具 | 来源 | 用途 | |---|---|---| | `read_file` / `write_file` / `edit_file` | FilesystemTool | 文件读写改 | | `grep_files` / `glob_files` / `list_files` | FilesystemTool | 文件检索 | | `execute` | ShellExecuteTool | 命令、`python xxx.py`、批量文件操作 | | `todo_write` | enableTaskList | 多步任务清单 | **Python 策略**:v1 不注册 `execute_python`(它绑定案件 DuckDB 快照、属于数据分析语义);Python 脚本用 `execute` 执行。若后续需要结构化 Python 工具,再加一个工作区 cwd 的 `run_python`,不依赖案件库。 ### 6.3 工作区与安全边界 - `project(workspace)` + `LocalFsMode.ROOTED`:绝对路径只能落在工作区、project、`addRoot` 内;`..`/越界抛 `SecurityException`。 - `addRoot` 只加 `{user.dir}/QingJian/chat-attachments/{userId}/{caseId}`,让任务 agent 能处理附件;不暴露整个仓库。 - **环境变量不要全量继承**:`inheritEnv` 默认 false;显式注入最小集合 `PATH/SystemRoot/SystemDrive/TEMP/TMP/COMSPEC/HOME/USERPROFILE/LANG/PYTHONIOENCODING`,避免把数据库密码等进程环境泄露给模型。 - `shell-enabled` 配置开关:关闭时 `disableShellTool()`,任务流水线退化为只读/写文件。 - 提示词层再加一道:禁止删除/覆盖前不确认、禁止访问工作区外路径、失败如实报告。 - 说明:现有数据分析 agent 本来就有 shell 能力,本次不新增攻击面;但把任务链路作为一等功能后,建议后续接 `SandboxFilesystemSpec`(Docker)做 OS 级隔离。本期先做路径/环境/开关三层约束。 ### 6.4 提示词 `prompts/task-agent.txt` 要点: 1. 角色:清鉴工作区里的通用任务助手; 2. 能力清单与工具使用优先级(有专用文件工具就不要用 shell); 3. 工作区规则:相对路径、禁止越界、附件路径可直接读; 4. 行为规则:闲聊直接回答不调工具;任务先规划再执行;删除/覆盖先确认;不编造执行结果; 5. 数据查询类问题(查案件库/统计业务数据)不在职责内,提示用户换数据分析模式——作为误路由的兜底; 6. 输出:中文 Markdown,结束时说明"做了什么/产物在哪/如何验证"。 ### 6.5 记忆隔离 - 两条流水线共用 `AgentStateStore` 实例,但 `RuntimeContext.sessionId` 不同:数据用 `{sessionId}`,任务用 `{sessionId}-task`。 - 原因:若共用槽位,数据链路的 `execute_sql` 等 tool_use 消息会被任务 agent 加载,而任务 agent 没有这些工具,部分厂商会直接拒绝请求。 - 代价:任务 agent 不直接看到数据链路的 agent 记忆;靠意图增强的 `enhanced` 问题 + DB 历史(分类器每次都读)补上下文。任务流水线首版不生成 followup 建议(`FollowupMiddleware` 只挂在数据 agent 上)。 --- ## 7. `AgentChatServiceImpl#doStreamLocked` 改造顺序 ```java // 1. 会话/附件(不变) // 2. 模型解析(不变)+ 显式校验模型存在(新增 AgentModelMapper.selectById) // 3. 意图路由(新增):RouteResult route = chatIntentRouter.route(...) // 4. 用户消息落库(从原位置后移:分类历史不再包含本轮问题) // 5. 组装 msgs:用户消息 +(有决策时) 合成消息 // 6. modelChanged 回写(模型已校验,可安全回写) // 7. events = intent 帧 + route.pipeline().stream(ctx) // 8. 原 SSE 事件转换/落库/心跳/取消逻辑保持不变 ``` 注意: - `IntentMiddleware` 里原来的 synthetic 消息注入逻辑抽成 `IntentMessages.analysis(decision)`,两条流水线共用。 - `agentRowId`、`RuntimeContext`、`SkillFilter` 的获取方式不变;`SkillFilter` 放进 `ChatPipelineContext`。 - 数据流水线的池 key、工具装配、提示词追加全部不变。 --- ## 8. 前端改动(最小集) - `src/ai/api/types.ts`:`SseIntentData` 增加 `code? / label? / pipeline?: 'data' | 'task'`。 - `src/ai/components/StreamStatusBar.vue`:`raw = intent.label || intent.intent`;tooltip 增加流水线名称;标签逻辑不变。 - `src/ai/utils/constants.ts`:`TOOL_LABELS` 增加 `read_file/write_file/edit_file/grep_files/glob_files/list_files/execute/todo_write` 等中文标签(否则时间线显示原始英文工具名)。 - `chatStream.ts` 已整对象存 `message.intent`,无需改;`sse.ts` 已转发 `intent`,无需改。 - 不加新页面、不加新模式选择器;用户无感知,只在状态栏看到"任务执行/通用问答"标签。 --- ## 9. 配置 ```yaml etl: intent: enabled: true route-enabled: true # false = 完全回到现状(中间件分类 + 单一流水线) timeout-seconds: 5 history-turns: 3 max-tokens: 512 task-agent: enabled: true max-iters: 20 pool-max: 32 idle-timeout-minutes: 30 shell-enabled: true shell-timeout-seconds: 120 max-output-bytes: 100000 skills-enabled: true ``` `IntentProperties` 增加 `routeEnabled`(Java 默认 false,yaml 显式开 true);`EtlProperties` 增加 `taskAgent`。 --- ## 10. 文件改动清单 **新增** - `module/agent/intent/IntentDecision.java` - `module/agent/intent/IntentFrames.java`(intent 帧 + `` 渲染) - `module/agent/intent/ChatIntentRouter.java` - `module/agent/pipeline/ChatPipeline.java` - `module/agent/pipeline/PipelineType.java` - `module/agent/pipeline/ChatPipelineContext.java` - `module/agent/pipeline/DataAnalysisPipeline.java` - `module/agent/pipeline/TaskPipeline.java` - `module/agent/task/TaskAgentFactory.java` - `module/agent/task/TaskAgentProperties.java` - `module/agent/task/TaskPromptBuilder.java` - `src/main/resources/prompts/task-agent.txt` **修改** - `intent/IntentEnum.java`、`IntentResult.java`、`IntentService.java`、`IntentMiddleware.java`、`IntentProperties.java` - `config/EtlProperties.java` - `service/AgentService.java`(条件挂载中间件) - `service/impl/AgentChatServiceImpl.java`(路由 + 模型校验 + 消息顺序) - `src/main/resources/application.yaml` - 前端 `types.ts`、`StreamStatusBar.vue`、`constants.ts` **测试** - 新增 `intent/IntentEnumTest`、`intent/IntentServiceTest`(mock LlmService)、`intent/ChatIntentRouterTest`、`task/TaskPromptBuilderTest`、`task/TaskAgentFactoryTest` - 回归 `service/AgentExpertTest`(若断言了中间件装配)、`tools/AgentToolRegistryTest` --- ## 11. 实施步骤 1. 意图层:`IntentEnum/IntentResult/IntentService/IntentDecision/IntentFrames` + 提示词重写 + 单测。 2. 抽流水线:`ChatPipeline/DataAnalysisPipeline`,`AgentChatServiceImpl` 先只走数据流水线,跑全量测试确认**零行为变化**。 3. 任务链路:`TaskAgentFactory/TaskPromptBuilder/TaskAgentProperties` + 配置 + 单测(断言 `LocalFilesystemSpec.getProject()/getMode()/getAdditionalRoots()`、shell 开关)。 4. 接路由:`ChatIntentRouter` + `doStreamLocked` 改造 + `AgentService` 条件中间件 + intent 帧。 5. 前端:类型、状态栏标签、工具中文名。 6. 配置开关与冒烟:数据分析 / 闲聊 / 任务(建文件、执行命令、跑 Python)/ 附件处理 / 回滚。 7. `mvn -pl ai-server test`。 --- ## 12. 测试与验收 **单元** - `fromAny("data_query")`、`fromAny("《数据查询》")`、未知 → null。 - 路由:6 个意图 → 预期 pipeline;null → data;`route-enabled=false` 时不调用 `IntentService`。 - `IntentService`:正常返回决策、LLM 抛异常 → null、未知 code → null。 - 任务提示词:含工作区规则/工具优先级/数据问题兜底,不含 SQL 契约。 - 任务工厂:路径策略、shell 开关、附件根。 **手工冒烟(SSE)** | 输入 | 期望 | |---|---| | `案件里有多少人?` | `intent=data_query`,数据流水线,SQL 工具 | | `你好` | `intent=general_chat`,任务流水线,不调工具,快速返回 | | `在当前工作区建 test.txt 写入 hello` | `intent=task_execute`,`write_file` 工具事件 | | `执行 python 脚本 demo.py` | `task_execute`,`execute` 工具事件 | | `统计附件 CSV 的金额分布` | `data_query`,数据流水线(execute_python) | | `把附件 CSV 按性别拆成两个文件` | `task_execute`,任务流水线 | | `route-enabled=false` 重启后 `你好` | 回到现状(中间件分类 + 数据 agent),intent 帧仍正常 | **回滚** - 一个配置项 `etl.intent.route-enabled=false` 即可完全回到当前行为;数据流水线代码虽被抽取,但 agent 构建、工具、提示词、SSE 全部保持原样。 --- ## 13. 风险与应对 1. **误分类**:分类器拿不准时强制 `data_query`;任务提示词对数据问题有兜底话术;后续可再加请求级手动指定(本期按确认不做)。 2. **Shell 安全**:路径策略 + 最小环境变量 + 超时/输出上限 + `shell-enabled` 开关 + 提示词约束;现有数据 agent 本就有 shell,不新增面。生产如需强隔离,后续切 `SandboxFilesystemSpec`(Docker)。 3. **记忆串台**:任务用 `-task` 后缀的独立记忆槽,避免跨工具集的历史污染;代价是跨流水线上下文依赖 `enhanced` 改写。 4. **意图识别延迟**:改用 `LlmService` 并加 5s 超时、关闭思考,失败直接回落,不阻塞主链路。 5. **兼容性**:intent 帧保留 `intent` 中文字段,新增 `code/label/pipeline`,老前端不受影响。 --- ## 14. 本期不做 - 会话级手动切换/锁定流水线(按确认走自动路由)。 - 任务流水线的 followup 建议。 - 任务工作区的文件浏览/下载 UI。 - Docker/OS 级沙箱。 - 把 `pipeline` 持久化进消息 metadata(可作为后续增强)。 --- ## 附:关键代码位置索引 | 主题 | 位置 | |---|---| | 意图分类入口 | `module/agent/intent/IntentService.java` | | 意图中间件 | `module/agent/intent/IntentMiddleware.java` | | 意图提示词 | `src/main/resources/prompts/intent-recognition.txt` | | agent 装配/中间件挂载 | `module/agent/service/AgentService.java`(`buildAgent`) | | 流式主链路 | `module/agent/service/impl/AgentChatServiceImpl.java`(`doStreamLocked`) | | SSE 事件转换 | 同上(`stream` / `doStreamLocked` 内 `concatMap`) | | 独立流水线先例 | `module/agent/insight/agent/InsightAgentFactory.java` | | 实例池 | `module/agent/service/AgentInstancePool.java` | | 工作区路径 | `module/agent/WorkspaceManagerFactory.java` | | Python 执行 | `module/agent/python/PythonExecutor.java` | | 附件存储根 | `module/agent/attachment/ChatAttachmentService.java`(`baseDir`) | | 前端 intent 渲染 | `ai-electron/frontend/src/ai/components/StreamStatusBar.vue` | | 前端工具标签 | `ai-electron/frontend/src/ai/utils/constants.ts` | | 前端 SSE 类型 | `ai-electron/frontend/src/ai/api/types.ts` |