状态:设计稿,未实施 日期:2026-10-20 范围:
ai-server/src/main/java/com/zsjz/ai/module/agent/(意图识别、闲聊/任务流水线) 关联需求:在现有「数据分析」AI 流水线之外,新增一条同时承接普通聊天与任务执行的流水线,由意图识别按条消息自动路由。
用户原话中的「人物」按同音词「任务」理解(已确认:"任务。")。要新增一条 AI 流水线,同时承接:
该流水线与现有数据分析流水线并存,按意图逐条消息自动路由。分类失败、超时、未知意图一律回落到数据分析流水线(保持现状,fail-safe)。现有数据分析 agent 的 67 个业务工具、提示词、SSE 契约全部不动。
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() 没有超时,模型卡住会阻塞整条流。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)。LocalFilesystemSpec,所以它本来就有 shell/文件工具。新流水线的价值是:去掉 55 个业务工具 + 数据 render 契约,换成任务型提示词与更严格的路径边界。InsightController + InsightAgentFactory + 静态 prompt),但它按 endpoint 隔离,不是按意图。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 的转换代码一行不改。
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 供路由使用。
IntentResult + 提示词IntentResult.intent 的语义改为 code(data_query 等),修掉过期的 《...》 注释。prompts/intent-recognition.txt:
data_query;纯问候/能力询问 → general_chat;只有明确的文件/命令/脚本操作才 task_execute;《》 强制要求、删除旧二分类段落和 response 字段;{format} 占位(schema 交给 LlmService.chatAs 追加),只保留 {multi_turn}。@JsonPropertyDescription 生成 schema。IntentService 改造LlmService,用 chatAs(request, IntentResult.class) 替换自建 ReActAgent:
enableThinking(false)(分类任务不需要思考,避免 reasoning 吃掉 token);maxTokens(props.maxTokens)、timeout(Duration.ofSeconds(props.timeoutSeconds)) —— 这两个配置终于生效;null(fail-open)。IntentDecision(IntentEnum intent, String enhanced, List<String> entities);fromAny 为 null 时返回 null。IntentMiddleware(只保留为回滚路径)IntentDecision,渲染时用 label,intent 帧仍下发中文 label(前端兼容)。AgentService.buildAgent 中条件挂载:
if (!etlProperties.getIntent().isRouteEnabled()) {
b.middleware(new IntentMiddleware(intentService, etlProperties.getIntent(), modelId));
}
路由开启时中间件不挂载,避免每轮分类两次。
新增 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 精确展示。
新增 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) {}
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);
TaskPipelineHarnessAgent 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);
module/agent/task/)TaskAgentFactory沿用 InsightAgentFactory + AgentInstancePool 的模式:
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(...);
@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,不依赖案件库。
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(),任务流水线退化为只读/写文件。SandboxFilesystemSpec(Docker)做 OS 级隔离。本期先做路径/环境/开关三层约束。prompts/task-agent.txt要点:
AgentStateStore 实例,但 RuntimeContext.sessionId 不同:数据用 {sessionId},任务用 {sessionId}-task。execute_sql 等 tool_use 消息会被任务 agent 加载,而任务 agent 没有这些工具,部分厂商会直接拒绝请求。enhanced 问题 + DB 历史(分类器每次都读)补上下文。任务流水线首版不生成 followup 建议(FollowupMiddleware 只挂在数据 agent 上)。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。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,无需改。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。
新增
module/agent/intent/IntentDecision.javamodule/agent/intent/IntentFrames.java(intent 帧 + <intent-analysis> 渲染)module/agent/intent/ChatIntentRouter.javamodule/agent/pipeline/ChatPipeline.javamodule/agent/pipeline/PipelineType.javamodule/agent/pipeline/ChatPipelineContext.javamodule/agent/pipeline/DataAnalysisPipeline.javamodule/agent/pipeline/TaskPipeline.javamodule/agent/task/TaskAgentFactory.javamodule/agent/task/TaskAgentProperties.javamodule/agent/task/TaskPromptBuilder.javasrc/main/resources/prompts/task-agent.txt修改
intent/IntentEnum.java、IntentResult.java、IntentService.java、IntentMiddleware.java、IntentProperties.javaconfig/EtlProperties.javaservice/AgentService.java(条件挂载中间件)service/impl/AgentChatServiceImpl.java(路由 + 模型校验 + 消息顺序)src/main/resources/application.yamltypes.ts、StreamStatusBar.vue、constants.ts测试
intent/IntentEnumTest、intent/IntentServiceTest(mock LlmService)、intent/ChatIntentRouterTest、task/TaskPromptBuilderTest、task/TaskAgentFactoryTestservice/AgentExpertTest(若断言了中间件装配)、tools/AgentToolRegistryTestIntentEnum/IntentResult/IntentService/IntentDecision/IntentFrames + 提示词重写 + 单测。ChatPipeline/DataAnalysisPipeline,AgentChatServiceImpl 先只走数据流水线,跑全量测试确认零行为变化。TaskAgentFactory/TaskPromptBuilder/TaskAgentProperties + 配置 + 单测(断言 LocalFilesystemSpec.getProject()/getMode()/getAdditionalRoots()、shell 开关)。ChatIntentRouter + doStreamLocked 改造 + AgentService 条件中间件 + intent 帧。mvn -pl ai-server test。单元
fromAny("data_query")、fromAny("《数据查询》")、未知 → null。route-enabled=false 时不调用 IntentService。IntentService:正常返回决策、LLM 抛异常 → null、未知 code → null。手工冒烟(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 全部保持原样。data_query;任务提示词对数据问题有兜底话术;后续可再加请求级手动指定(本期按确认不做)。shell-enabled 开关 + 提示词约束;现有数据 agent 本就有 shell,不新增面。生产如需强隔离,后续切 SandboxFilesystemSpec(Docker)。-task 后缀的独立记忆槽,避免跨工具集的历史污染;代价是跨流水线上下文依赖 enhanced 改写。LlmService 并加 5s 超时、关闭思考,失败直接回落,不阻塞主链路。intent 中文字段,新增 code/label/pipeline,老前端不受影响。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 |