intent-routing-chat-task-pipeline.md 21 KB

普通聊天 + 任务流水线(意图路由)设计方案

状态:设计稿,未实施 日期: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-analysis> 合成消息、发 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<AgentEvent>,因此 AgentChatServiceImpl 里那一大段事件→SSE 的转换代码一行不改。


3. 意图层改造

3.1 IntentEnum(新增枚举值与流水线归属)

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<String> entities);fromAny 为 null 时返回 null。

3.4 IntentMiddleware(只保留为回滚路径)

  • 改为消费 IntentDecision,渲染时用 label,intent 帧仍下发中文 label(前端兼容)。
  • AgentService.buildAgent 中条件挂载:

    if (!etlProperties.getIntent().isRouteEnabled()) {
    b.middleware(new IntentMiddleware(intentService, etlProperties.getIntent(), modelId));
    }
    
  • 路由开启时中间件不挂载,避免每轮分类两次。


4. 路由层

新增 module/agent/intent/ChatIntentRouter.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) 统一生成:

{"type":"intent","code":"task_execute","label":"任务执行","intent":"任务执行",
 "pipeline":"task","enhanced":"...","entities":[]}

保留 intent = label 是前端兼容做法;新增 code/label/pipeline 供 UI 精确展示。


5. 流水线抽象

新增 module/agent/pipeline/:

public interface ChatPipeline {
    PipelineType type();                       // DATA_ANALYSIS / TASK
    Flux<AgentEvent> stream(ChatPipelineContext ctx);
}

public record ChatPipelineContext(
    Long sessionId, Long caseId, Long modelId, Long agentRowId,
    String userId, SkillFilter skillFilter,
    String userText, List<Msg> messages, IntentDecision intent) {}

5.1 DataAnalysisPipeline

把现有 doStreamLocked 里的 agent 获取 + RuntimeContext 构建搬进来,行为保持不变:

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

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 冲突)。
  • 构建:

    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 改造顺序

// 1. 会话/附件(不变)
// 2. 模型解析(不变)+ 显式校验模型存在(新增 AgentModelMapper.selectById)
// 3. 意图路由(新增):RouteResult route = chatIntentRouter.route(...)
// 4. 用户消息落库(从原位置后移:分类历史不再包含本轮问题)
// 5. 组装 msgs:用户消息 +(有决策时)<intent-analysis> 合成消息
// 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. 配置

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 帧 + <intent-analysis> 渲染)
  • 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