From 5097d25379d5a36d5c1900e7f6faed8cacaffa79 Mon Sep 17 00:00:00 2001 From: YeYuheng <57035043+YYH211@users.noreply.github.com> Date: Thu, 18 Dec 2025 13:42:59 +0800 Subject: [PATCH] skill tool config (#6114) Co-authored-by: xxyyh <2289112474@qq> --- .../HelperBot/dispatch/skillAgent/prompt.ts | 182 ++++++++++-------- .../core/workflow/dispatch/ai/agent/index.ts | 2 + .../dispatch/ai/agent/skillMatcher.ts | 96 +++++---- .../dispatch/ai/agent/sub/plan/index.ts | 2 + .../dispatch/ai/agent/sub/plan/prompt.ts | 31 ++- .../app/detail/Edit/ChatAgent/EditForm.tsx | 24 ++- .../Edit/ChatAgent/SkillEdit/ChatTest.tsx | 99 ++++++++-- .../app/detail/Edit/ChatAgent/utils.ts | 48 +++++ .../app/src/pages/api/core/ai/skill/detail.ts | 12 +- 9 files changed, 353 insertions(+), 143 deletions(-) diff --git a/packages/service/core/chat/HelperBot/dispatch/skillAgent/prompt.ts b/packages/service/core/chat/HelperBot/dispatch/skillAgent/prompt.ts index 8c108d564..ec2783fa2 100644 --- a/packages/service/core/chat/HelperBot/dispatch/skillAgent/prompt.ts +++ b/packages/service/core/chat/HelperBot/dispatch/skillAgent/prompt.ts @@ -13,26 +13,27 @@ export const getPrompt = ({ return ` -你是一个**任务执行流程设计专家**,专门将任务目标转化为清晰的顺序执行步骤。 +规划生成机器人是一个**战略规划专家**,专门从宏观视角设计任务执行框架。 -**核心价值**:提供简洁、清晰、可执行的步骤列表,帮助用户完成任务。 +**核心价值**:规划生成机器人提供纲领性、方向性的执行框架,为后续详细规划奠定基础。 **核心能力**: -- 任务分解:将任务拆解为清晰的执行步骤 -- 工具匹配:为每个步骤选择合适的工具 -- 流程组织:按照逻辑顺序组织步骤 +- 战略分析:从宏观层面理解任务的本质和目标 +- 框架设计:将任务划分为几个关键阶段或模块 +- 资源规划:识别每个阶段需要的资源类型 -**核心目标**:设计一个顺序执行的流程来完成任务,包含: -1. **清晰的步骤**:每个步骤说明要做什么 -2. **工具指定**:每个步骤使用哪个工具 -3. **顺序执行**:从第一步到最后一步,依次执行 +**核心目标**:设计一个纲领性的执行框架来指导任务完成,包含: +1. **宏观阶段**:将任务划分为几个关键执行阶段(而非详细步骤) +2. **资源方向**:每个阶段需要什么类型的工具/知识库 +3. **战略顺序**:阶段之间的逻辑关系 **输出价值**: -- 用户可以按照步骤顺序完成任务 -- 每个步骤明确使用的工具 -- 流程简单直接,易于理解 +- 提供任务执行的宏观蓝图和方向 +- 作为后续详细规划的参考框架 +- 类似"五年规划"的纲领性文档,而非具体操作手册 +- 具体的执行细节将在实际运行时由系统收集 ${currentConfigContext} @@ -40,52 +41,57 @@ ${currentConfigContext} **如果有前置信息**(任务目标、工具列表等): - 这些是已经确定的,不需要重复询问 -- 你的任务是将它们组织成顺序执行的步骤 +- 规划生成机器人的任务是将它们组织成顺序执行的阶段 - 重点是"按什么顺序做什么" **系统功能处理**(如果预设计划中包含系统功能配置): -如果预设计划中已包含系统功能配置(如文件上传),你需要: +如果预设计划中已包含系统功能配置(如文件上传),规划生成机器人需要: 1. **识别已启用的系统功能** - 检查 resources.system_features 中哪些功能已启用 - 理解每个系统功能的目的和使用场景 2. **在步骤设计中考虑系统功能** - - 如果文件上传已启用:在第一步或相关步骤中说明需要用户上传文件 - - 在步骤描述中明确说明文件的作用和处理方式 - - 考虑文件处理相关的步骤(如数据提取、格式转换等) + - 如果文件上传已启用:在第一阶段或相关阶段中说明需要用户上传文件 + - 在阶段描述中明确说明文件的作用和处理方式 + - 考虑文件处理相关的阶段(如数据提取、格式转换等) 3. **系统功能不在 expected_tools 中** - 系统功能是平台级配置,不是工具调用 - expected_tools 中只包含实际执行操作的工具和知识库 - - 系统功能已在前期确定,步骤设计时只需要考虑其影响即可 + - 系统功能已在前期确定,阶段设计时只需要考虑其影响即可 当处于信息收集阶段时: -**信息收集目标**:收集设计执行步骤所需的信息: +**信息收集目标**:从宏观层面收集战略规划所需的信息: -1. **步骤分解** - - 完成任务需要哪些步骤? - - 每个步骤的目的是什么? - - 步骤的执行顺序是什么? +1. **任务定位与场景** + - 这个任务要解决什么核心问题? + - 主要面向什么样的应用场景? + - 任务的最终目标是什么? -2. **工具使用** - - 每个步骤应该使用哪个工具? - - 是否有步骤需要使用多个工具? +2. **资源方向识别** + - 需要什么类型的能力?(信息获取/数据处理/内容生成/服务集成) + - 是否需要特定领域的知识支持? + - 是否需要与外部系统交互? -3. **流程细节** - - 任务的起点是什么? - - 任务的终点是什么? - - 是否需要中间验证步骤? +3. **战略约束** + - 任务有什么重要的限制条件? + - 是否有特定的用户群体或使用场景? + - 期望的复杂度水平如何? **关键原则**: -- **简单直接**:不考虑复杂的分支和条件,就是顺序执行 -- **基于已知**:使用已确定的工具列表 -- **用户友好**:步骤描述清晰,容易理解 +- **宏观视角**:关注"做什么"而非"怎么做",避免询问具体实现细节 +- **战略导向**:了解任务的本质和方向,而非具体步骤 +- **框架思维**:收集的信息用于设计执行框架,具体操作留给后续阶段 + +**提问层次要求**: +- ✅ 宏观层面:任务目标、应用场景、资源类型、战略方向 +- ❌ 具体细节:具体参数、详细流程、技术实现、操作步骤 **输出格式**: @@ -94,36 +100,42 @@ ${currentConfigContext} 直接输出以下格式的JSON(不要添加代码块标记): { "phase": "collection", - "reasoning": "为什么问这个问题的推理过程:基于什么考虑、希望收集什么信息、对后续有什么帮助", + "reasoning": "为什么问这个问题的推理过程:基于什么战略考虑、希望收集什么方向性信息", "question": "实际向用户提出的问题内容" } 问题内容可以是开放式问题,也可以包含选项: -开放式问题示例: +✅ **好的宏观提问示例**: { "phase": "collection", - "reasoning": "需要首先了解任务的基本定位和目标场景,这将决定后续需要确认的工具类型和步骤设计", - "question": "我想了解一下您希望这个执行流程实现什么功能?能否详细描述一下具体要处理什么样的任务?" + "reasoning": "需要了解任务的核心定位和战略目标,这将决定整体框架的设计方向", + "question": "这个 Skill 主要想解决什么问题?比如是面向客户服务、内容创作、数据分析,还是其他应用场景?" } -选择题示例: { "phase": "collection", - "reasoning": "需要确认步骤设计的重点方向,这将影响流程的详细程度", - "question": "关于流程的设计,您更关注:\\nA. 步骤的详细程度(每个步骤都很详细)\\nB. 步骤的灵活性(可以根据情况调整)\\nC. 步骤的简洁性(尽可能少的步骤)\\nD. 其他\\n\\n请选择最符合的选项,或输入您的详细回答:" + "reasoning": "需要识别任务所需的核心能力类型,这将决定资源配置的大方向", + "question": "从功能角度看,这个任务主要需要:\\nA. 获取和整合信息(搜索、查询、知识检索)\\nB. 处理和分析数据(计算、转换、分析)\\nC. 生成和创作内容(文档、报告、创意)\\nD. 集成外部服务(API调用、消息通知)\\n\\n请选择最主要的方向,或描述您的理解:" } +❌ **不好的具体提问示例**(避免这样问): +- "第一步需要调用哪个API?" +- "数据格式应该是JSON还是XML?" +- "需要设置多少个验证节点?" +- "具体的搜索关键词是什么?" + 适合选择题的场景: -- 经验水平判断(初学者/有经验/熟练/专家) -- 优先级排序(时间/质量/成本/创新) -- 任务类型分类(分析/设计/开发/测试) -- 复杂度判断(简单/中等/复杂/极复杂) +- 任务类型分类(客服/创作/分析/集成) +- 核心能力方向(信息获取/数据处理/内容生成/服务集成) +- 复杂度定位(简单直接/中等复杂/综合复杂) +- 应用场景(内部使用/客户服务/自动化流程) -避免的行为: -- 不要为所有问题都强制提供选项 -- 选项之间要有明显的区分度 -- 不要使用过于技术化的术语 +提问建议: +- 每个问题聚焦一个战略维度 +- 用通俗语言而非技术术语 +- 提供选项时给出清晰的场景描述 +- 避免过早深入实现细节 @@ -191,12 +203,14 @@ ${currentConfigContext} ${resourceList} """ -**计划生成要求**: +**纲领性规划要求**: 1. 严格按照JSON格式输出 2. **严格确保所有引用的工具都在可用工具列表中** - 这是硬性要求 -3. 考虑搭建者的实际约束条件(时间、资源、技能等) -4. 计划要具体、可执行、步骤清晰,适合作为流程模板 -5. **绝不要使用任何不在可用工具列表中的工具** - 违背此项将导致计划被拒绝 +3. 生成的是宏观执行框架,而非详细操作步骤 +4. **重要**:这是一个"五年规划"式的纲领文档,具体细节由后续User Mode收集 +5. 步骤数量建议3-5个,每个步骤代表一个关键阶段或模块 +6. 步骤描述要简洁、方向性,避免过于具体的操作指令 +7. **绝不要使用任何不在可用工具列表中的工具** - 违背此项将导致计划被拒绝 **🚨 资源使用严格限制(极其重要)**: @@ -218,7 +232,7 @@ ${resourceList} - ❌ 不要猜测 type 值,必须根据列表中的标签确定 **输出前的自我检查步骤**: -1. 查看你选择的每个资源ID,它在列表中的标签是什么? +1. 查看规划生成机器人选择的每个资源ID,它在列表中的标签是什么? 2. 如果标签是 [工具] → 设置 "type": "tool" 3. 如果标签是 [知识库] → 设置 "type": "knowledge" 4. 确保每个资源都有 id 和 type 两个字段 @@ -288,6 +302,20 @@ ${resourceList} - name: 简洁的英文计划名称,使用驼峰命名或下划线,例如:"createMarketingReport", "analyzeCustomerData" - description: 详细描述计划的功能、使用场景和输出结果,用于智能匹配用户需求 - description应该包含:功能说明、适用场景、预期输出、关键特性等 +- **steps数量**: 建议3-5个关键阶段,不要过于细化 + +**纲领性步骤设计原则**: +1. **阶段而非步骤**:每个step代表一个关键阶段或模块,而非具体操作 +2. **方向而非细节**:描述"做什么"的方向,而非"怎么做"的细节 +3. **资源类型**:标注需要的工具/知识库类型,具体参数在User Mode收集 +4. **简洁描述**:一句话说明阶段目标,避免冗长的操作说明 + +**步骤描述层次对比**: +- ✅ 纲领性:"收集用户需求信息" (宏观方向) +- ❌ 详细化:"通过表单收集用户的姓名、邮箱、电话,并验证格式" (过于具体) + +- ✅ 纲领性:"分析和处理数据" (宏观方向) +- ❌ 详细化:"先过滤空值,然后按时间排序,最后计算平均值" (过于具体) 资源使用格式: - 在description中使用@[资源名称]格式引用资源 @@ -297,19 +325,13 @@ ${resourceList} - 确保资源引用的准确性 - 优先选择功能最匹配的资源 -工具选择原则: -1. 功能匹配优先:选择最能满足步骤需求的工具 -2. 组合优化:多个工具可以组合使用以获得更好效果 -3. 逻辑连贯:确保工具选择的逻辑性 -4. 简洁高效:避免不必要的工具冗余 - -**✅ 正确示例**: +**✅ 纲领性规划示例**: { "phase": "generation", "plan_analysis": { - "name": "createTravelItinerary", - "description": "创建旅游行程计划,包括景点查询、天气预报、行程文档生成。适用于需要规划旅行的场景,输出完整的markdown格式行程文档。", - "goal": "为用户规划详细的旅游行程", + "name": "travelPlanning", + "description": "旅游行程规划助手,帮助用户规划旅行路线。整合目的地信息、天气数据,生成完整行程方案。", + "goal": "为用户提供完整的旅游行程规划方案", "type": "内容生成" }, "execution_plan": { @@ -317,24 +339,24 @@ ${resourceList} "steps": [ { "id": "step1", - "title": "查询旅游目的地信息", - "description": "使用@[travel_destinations]知识库查询目的地的景点、美食、住宿等详细信息", + "title": "收集目的地基础信息", + "description": "通过@[travel_destinations]获取目的地相关信息", "expectedTools": [ {"id": "travel_destinations", "type": "knowledge"} ] }, { "id": "step2", - "title": "查询实时天气信息", - "description": "使用@[mojiWeather/tool]工具获取目的地未来7天的天气预报", + "title": "获取实时环境数据", + "description": "使用@[mojiWeather/tool]获取天气等环境信息", "expectedTools": [ {"id": "mojiWeather/tool", "type": "tool"} ] }, { "id": "step3", - "title": "生成行程计划文档", - "description": "使用@[markdownTransform]工具将行程信息格式化为markdown文档", + "title": "生成行程方案", + "description": "整合信息并使用@[markdownTransform]生成最终方案", "expectedTools": [ {"id": "markdownTransform", "type": "tool"} ] @@ -374,33 +396,33 @@ ${resourceList} **🎯 关键:如何判断当前应该处于哪个阶段** -**每次回复前,你必须自主评估以下问题**: +**每次回复前,规划生成机器人必须自主评估以下问题**: 1. **信息充分性评估**: - - 我是否已经明确了解要完成的任务目标? - - 我是否知道需要使用哪些工具和资源? - - 我是否了解任务的关键约束条件? + - 规划生成机器人是否已经明确了解要完成的任务目标? + - 规划生成机器人是否知道需要使用哪些工具和资源? + - 规划生成机器人是否了解任务的关键约束条件? - 如果上述问题有任何不确定,应该输出 \`"phase": "collection"\` 继续提问 2. **配置生成时机判断**: - 满足以下**所有条件**时,才能输出 \`"phase": "generation"\`: * 已经明确任务的核心目标和场景 * 已经确认可用的工具和资源 - * 已经收集到足够信息来设计执行步骤 + * 已经收集到足够信息来设计执行阶段 * 对话轮次达到 2-4 轮(避免过早生成) 3. **阶段回退机制**: - 如果用户在配置生成后继续发送消息 - 评估新信息: - * 如果是小调整(修改步骤、工具选择等)→ 输出 \`"phase": "generation"\` 生成新计划 + * 如果是小调整(修改阶段、工具选择等)→ 输出 \`"phase": "generation"\` 生成新计划 * 如果发现核心需求变化或信息不足 → 输出 \`"phase": "collection"\` 回退继续提问 **重要原则**: -- ❌ 不要在第一轮对话就生成计划(除非用户提供了极其详细的需求) -- ❌ 不要在信息不足时强行生成计划 -- ✅ 宁可多问一两个问题,也不要生成不准确的计划 -- ✅ 当确信信息充分时,果断切换到计划生成阶段 -- ✅ 支持灵活的阶段切换,包括从计划生成回退到信息收集 +- ❌ 规划生成机器人不要在第一轮对话就生成计划(除非用户提供了极其详细的需求) +- ❌ 规划生成机器人不要在信息不足时强行生成计划 +- ✅ 规划生成机器人宁可多问一两个问题,也不要生成不准确的计划 +- ✅ 当确信信息充分时,规划生成机器人果断切换到计划生成阶段 +- ✅ 规划生成机器人支持灵活的阶段切换,包括从计划生成回退到信息收集 diff --git a/packages/service/core/workflow/dispatch/ai/agent/index.ts b/packages/service/core/workflow/dispatch/ai/agent/index.ts index 94a7a3410..e0d8abba3 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/index.ts @@ -228,6 +228,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise }); } if (plan) { + console.log('plan output', plan); workflowStreamResponse?.({ event: SseResponseEventEnum.agentPlan, data: { agentPlan: plan } @@ -362,6 +363,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise teamId: runningUserInfo.teamId, appId: runningAppInfo.id, userInput: lastInteractive ? interactiveInput : userChatInput, + messages: historiesMessages, // 传入完整的对话历史 model }); if (matchResult.matched && matchResult.systemPrompt) { diff --git a/packages/service/core/workflow/dispatch/ai/agent/skillMatcher.ts b/packages/service/core/workflow/dispatch/ai/agent/skillMatcher.ts index 42998b47e..f928694ff 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/skillMatcher.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/skillMatcher.ts @@ -8,7 +8,7 @@ import { getLLMModel } from '../../../../ai/model'; * 生成唯一函数名 * 参考 MatcherService.ts 的 _generateUniqueFunctionName */ -const generateUniqueFunctionName = (skill: HelperBotGeneratedSkillType): string => { +const generateUniqueFunctionName = (skill: AiSkillSchemaType): string => { let baseName = skill.name || skill._id.toString(); // 清理名称 @@ -30,13 +30,13 @@ const generateUniqueFunctionName = (skill: HelperBotGeneratedSkillType): string * 参考 MatcherService.ts 的 match 函数 */ export const buildSkillTools = ( - skills: HelperBotGeneratedSkillType[] + skills: AiSkillSchemaType[] ): { tools: ChatCompletionTool[]; - skillsMap: Record; + skillsMap: Record; } => { const tools: ChatCompletionTool[] = []; - const skillsMap: Record = {}; + const skillsMap: Record = {}; for (const skill of skills) { // 生成唯一函数名 @@ -67,26 +67,28 @@ export const buildSkillTools = ( * 格式化 Skill 为 SystemPrompt * 将匹配到的 skill 格式化为 XML 提示词 */ -export const formatSkillAsSystemPrompt = (skill: HelperBotGeneratedSkillType): string => { - let prompt = '\n'; - prompt += `**参考技能**: ${skill.name}\n\n`; +export const formatSkillAsSystemPrompt = (skill: AiSkillSchemaType): string => { + const lines = ['', `**参考技能**: ${skill.name}`, '']; if (skill.description) { - prompt += `**描述**: ${skill.description}\n\n`; + lines.push(`**描述**: ${skill.description}`, ''); } if (skill.steps && skill.steps.trim()) { - prompt += `**步骤信息**:\n${skill.steps}\n\n`; + lines.push(`**步骤信息**:`, skill.steps, ''); } - prompt += '**说明**:\n'; - prompt += '1. 以上是用户之前保存的类似任务的执行计划\n'; - prompt += '2. 请参考该技能的步骤流程和工具选择\n'; - prompt += '3. 根据当前用户的具体需求,调整和优化计划\n'; - prompt += '4. 保持步骤的逻辑性和完整性\n'; - prompt += '\n'; + lines.push( + '**说明**:', + '1. 以上是用户之前保存的类似任务的执行框架', + '2. 请参考该技能的宏观阶段划分和资源方向', + '3. 根据当前用户的具体需求,调整和优化框架', + '4. 保持阶段的逻辑性和方向的清晰性', + '', + '' + ); - return prompt; + return lines.join('\n'); }; /** @@ -97,45 +99,42 @@ export const matchSkillForPlan = async ({ teamId, appId, userInput, + messages, model }: { teamId: string; appId: string; userInput: string; + messages?: ChatCompletionMessageParam[]; model: string; }): Promise<{ matched: boolean; - skill?: HelperBotGeneratedSkillType; + skill?: AiSkillSchemaType; systemPrompt?: string; reason?: string; }> => { try { - // 1. 查询用户的 skills (使用 teamId 和 appId) const skills = await MongoAiSkill.find({ teamId, - appId, - status: { $in: ['active', 'draft'] } + appId }) .sort({ createTime: -1 }) - .limit(50) // 限制数量,避免 tools 过多 + .limit(50) .lean(); - console.log('skill list length', skills.length); - console.log('skill', skills); + if (!skills || skills.length === 0) { return { matched: false, reason: 'No skills available' }; } - // 2. 构建 tools 数组 const { tools, skillsMap } = buildSkillTools(skills); - console.log('tools', tools); + console.debug('tools', tools); - // 3. 获取模型配置 const modelData = getLLMModel(model); // 4. 调用 LLM Tool Calling 进行匹配 // 构建系统提示词,指导 LLM 选择相似的任务 - const systemPrompt = `你是一个智能任务匹配助手。请根据用户的当前需求,从提供的技能工具集中选择最相似的任务。 + const skillMatchSystemPrompt = `你是一个智能任务匹配助手。请根据用户的当前需求,从提供的技能工具集中选择最相似的任务。 **匹配原则**: 1. **任务目标相似性**:选择与用户目标最匹配的技能 @@ -148,22 +147,34 @@ export const matchSkillForPlan = async ({ - 如果有多个相似技能,选择最符合主要目标的那个 - 如果没有找到完全匹配的技能,选择功能最相近的 - 请从以下工具中选择一个最匹配的:`; + 请从以下工具中选择一个最匹配的: + + 工具匹配上直接使用工具调用的形式而不是文本描述的形式来进行返回`; - // 构建简化的消息,只包含系统提示词和用户输入 - const allMessages = [ + const allMessages: ChatCompletionMessageParam[] = [ { role: 'system' as const, - content: systemPrompt - }, - { - role: 'user' as const, - content: userInput + content: skillMatchSystemPrompt } ]; - console.log('match request', { userInput, skillCount: skills.length }); - const { toolCalls } = await createLLMResponse({ + if (messages && messages.length > 0) { + allMessages.push(...messages); + } + + allMessages.push({ + role: 'user' as const, + content: userInput + }); + + console.debug('match request', { + hasHistory: !!(messages && messages.length > 0), + historyCount: messages?.length || 0, + currentInput: userInput.substring(0, 100), // 只显示前100个字符 + skillCount: skills.length + }); + + const llmResponse = await createLLMResponse({ body: { model: modelData.model, messages: allMessages, @@ -174,6 +185,17 @@ export const matchSkillForPlan = async ({ } }); + console.log('=== LLM 完整返回 ==='); + console.log('完整响应 keys:', Object.keys(llmResponse)); + console.log('toolCalls:', llmResponse.toolCalls); + console.log('assistantMessage:', llmResponse.assistantMessage); + + // 打印完整对象(过滤掉可能很长的字段) + const { assistantMessage, ...otherFields } = llmResponse; + console.log('其他返回字段:', JSON.stringify(otherFields, null, 2)); + + const { toolCalls } = llmResponse; + // 5. 解析匹配结果 if (toolCalls && toolCalls.length > 0) { const toolCall = toolCalls[0]; diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/index.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/index.ts index 9ba1c03ac..5420d0878 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/index.ts @@ -77,7 +77,9 @@ export const dispatchPlanAgent = async ({ // 分类:query/user select/user form const lastMessages = requestMessages[requestMessages.length - 1]; + console.log('--------------PLAN MODE--------------'); console.log('user input:', userInput); + console.log('systemPrompt:', systemPrompt); // 上一轮是 Ask 模式,进行工具调用拼接 if ( diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/prompt.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/prompt.ts index 455e56eff..c17002911 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/plan/prompt.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/plan/prompt.ts @@ -50,6 +50,15 @@ export const getPlanAgentSystemPrompt = ({ ${subAppPrompt} 「以下是在规划 PLAN 过程中可以用来调用的工具,不应该在 step 的 description 中」 - [@${SubAppIds.ask}]:${PlanAgentAskTool.function.description} + + **工具选择限制**: + 1. **同类工具去重**:如果有多个功能相似的工具(如多个搜索工具、多个翻译工具等),只选择一个最合适的 + 2. **避免功能重叠**:不要在同一个计划中使用多个功能重叠的工具 + 3. **优先使用参考工具**:如果用户提供了背景信息/前置规划信息,优先使用其中已经使用的工具 + + 示例: + - 如果有 bing/webSearch、google/search、metaso/metasoSearch 等多个搜索工具,只选择一个 + - 如果背景信息中使用了 @tavily_search,则优先继续使用 @tavily_search 而不是切换到其他搜索工具 1. **前置信息检查**: @@ -252,7 +261,10 @@ export const getUserContent = ({ }) => { let userContent = `用户输入:${userInput}`; if (systemPrompt) { - userContent += `\n\n背景信息:${parseSystemPrompt({ systemPrompt, getSubAppInfo })}\n请按照用户提供的背景信息来重新生成计划,优先遵循用户的步骤安排和偏好。`; + userContent += `\n\n背景信息:${parseSystemPrompt({ systemPrompt, getSubAppInfo })}\n + 请按照用户提供的背景信息来重新生成计划,优先遵循用户的步骤安排和偏好。\n + + **重要**:如果背景信息中包含工具引用(@工具名),请优先使用这些工具。当有多个同类工具可选时(如多个搜索工具),优先选择背景信息中已使用的工具,避免功能重叠。`; } return userContent; }; @@ -297,6 +309,11 @@ export const getReplanAgentSystemPrompt = ({ ${subAppPrompt} 「以下是在规划 PLAN 过程中可以用来调用的工具,不应该在 step 的 description 中」 - [@${SubAppIds.ask}]:${PlanAgentAskTool.function.description} + +**工具选择限制**: +1. **工具一致性**:在追加优化步骤时,应优先使用已完成步骤中使用过的工具 +2. **同类工具去重**:避免引入新的同类工具,保持工具选择的一致性 +3. **功能不重叠**:不要选择与已使用工具功能重叠的其他工具 @@ -494,6 +511,14 @@ export const getReplanAgentUserPrompt = ({ ${stepsResponsePrompt} - 请基于上述关键步骤 ${stepsIdPrompt} 的执行结果,生成能够进一步优化和完善整个任务目标的追加步骤,如果有「用户前置规划」请按照用户的前置规划来重新生成计划,优先遵循用户的步骤安排和偏好。。 - 如果「关键步骤执行结果」已经满足了当前的「任务目标」,请直接返回一个总结的步骤来提取最终的答案,而不需要进行其他的讨论`; + 请基于上述关键步骤 ${stepsIdPrompt} 的执行结果,生成能够进一步优化和完善整个任务目标的追加步骤。 + + 如果有「用户前置规划」,请按照用户的前置规划来重新生成计划,优先遵循用户的步骤安排和偏好。 + + **工具选择原则**: + - 如果前面的步骤或「用户前置规划」中使用了某个工具,后续步骤应优先继续使用相同的工具 + - 避免在后续步骤中切换到功能相似的其他工具 + - 同类工具只选择一个,避免功能重叠 + + 如果「关键步骤执行结果」已经满足了当前的「任务目标」,请直接返回一个总结的步骤来提取最终的答案,而不需要进行其他的讨论。`; }; diff --git a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/EditForm.tsx b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/EditForm.tsx index cfe4f85bc..e706399e4 100644 --- a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/EditForm.tsx +++ b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/EditForm.tsx @@ -33,6 +33,7 @@ import { cardStyles } from '../../constants'; import { SmallAddIcon } from '@chakra-ui/icons'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { getAiSkillDetail } from '@/web/core/ai/skill/api'; +import { validateToolConfiguration, getToolConfigStatus } from './utils'; const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal')); const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal')); @@ -103,19 +104,36 @@ const EditForm = ({ } }, [selectedModel, setAppForm]); - // 打开编辑器 + // 打开skill编辑器 const handleEditSkill = useCallback( async (skill: SkillEditType) => { // If skill has dbId, load full details from server if (skill.id) { const detail = await getAiSkillDetail({ id: skill.id }); + + // Validate tools and determine their configuration status + const toolsWithStatus = (detail.tools || []) + .filter((tool) => { + // First, validate tool compatibility with current config + const isValid = validateToolConfiguration({ + toolTemplate: tool, + canSelectFile: appForm.chatConfig.fileSelectConfig?.canSelectFile, + canSelectImg: appForm.chatConfig.fileSelectConfig?.canSelectImg + }); + return isValid; + }) + .map((tool) => ({ + ...tool, + configStatus: getToolConfigStatus(tool) + })); + // Merge server data with local data onEditSkill({ id: detail._id, name: detail.name, description: detail.description || '', stepsText: detail.steps, - selectedTools: detail.tools || [], + selectedTools: toolsWithStatus, dataset: { list: detail.datasets || [] } }); } else { @@ -123,7 +141,7 @@ const EditForm = ({ onEditSkill(skill); } }, - [onEditSkill] + [onEditSkill, appForm.chatConfig.fileSelectConfig] ); return ( diff --git a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/ChatTest.tsx b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/ChatTest.tsx index 8558bfb49..a33084b30 100644 --- a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/ChatTest.tsx +++ b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/ChatTest.tsx @@ -3,12 +3,14 @@ import { useTranslation } from 'next-i18next'; import React, { useMemo } from 'react'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyIcon from '@fastgpt/web/components/common/Icon'; -import type { SkillEditType } from '@fastgpt/global/core/app/formEdit/type'; +import type { SkillEditType, SelectedToolItemType } from '@fastgpt/global/core/app/formEdit/type'; import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type'; import MyBox from '@fastgpt/web/components/common/MyBox'; import HelperBot from '@/components/core/chat/HelperBot'; import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type'; import { useToast } from '@fastgpt/web/hooks/useToast'; +import { getToolPreviewNode } from '@/web/core/app/api/tool'; +import { validateToolConfiguration, checkNeedsUserConfiguration } from '../utils'; type Props = { skill: SkillEditType; @@ -59,26 +61,85 @@ const ChatTest = ({ skill, appForm, onAIGenerate }: Props) => { { + onApply={async (generatedSkillData) => { console.log(generatedSkillData, 222); - // const stepsText = generatedSkillData.execution_plan.steps - // .map((step, index) => { - // let stepText = `步骤 ${index + 1}: ${step.title}\n${step.description}`; - // if (step.expectedTools && step.expectedTools.length > 0) { - // const tools = step.expectedTools - // .map((tool) => `${tool.type === 'tool' ? '🔧' : '📚'} ${tool.id}`) - // .join(', '); - // stepText += `\n使用工具: ${tools}`; - // } - // return stepText; - // }) - // .join('\n\n'); - // onAIGenerate({ - // name: generatedSkillData.plan_analysis.name || skill.name, - // description: generatedSkillData.plan_analysis.description || skill.description, - // stepsText: stepsText - // }); + // 1. 提取所有步骤中的工具 ID(去重,仅保留 type='tool') + const allToolIds = new Set(); + generatedSkillData.execution_plan.steps.forEach((step) => { + step.expectedTools?.forEach((tool) => { + if (tool.type === 'tool') { + allToolIds.add(tool.id); + } + }); + }); + + // 2. 并行获取工具详情 + const targetToolIds = Array.from(allToolIds); + const newTools: SelectedToolItemType[] = []; + const failedToolIds: string[] = []; + + if (targetToolIds.length > 0) { + const results = await Promise.all( + targetToolIds.map((toolId: string) => + getToolPreviewNode({ appId: toolId }) + .then((tool) => ({ status: 'fulfilled' as const, toolId, tool })) + .catch((error) => ({ status: 'rejected' as const, toolId, error })) + ) + ); + + results.forEach((result) => { + if (result.status === 'fulfilled') { + // 验证工具配置 + const toolValid = validateToolConfiguration({ + toolTemplate: result.tool, + canSelectFile: appForm.chatConfig.fileSelectConfig?.canSelectFile, + canSelectImg: appForm.chatConfig.fileSelectConfig?.canSelectImg + }); + + if (toolValid) { + // 判断是否需要用户配置,设置 configStatus + const needsConfig = checkNeedsUserConfiguration(result.tool); + newTools.push({ + ...result.tool, + configStatus: needsConfig ? 'waitingForConfig' : 'active' + }); + } else { + // 工具验证失败,记录失败 + failedToolIds.push(result.toolId); + } + } else if (result.status === 'rejected') { + failedToolIds.push(result.toolId); + } + }); + + // 可选:提示用户哪些工具获取失败 + if (failedToolIds.length > 0) { + console.warn('部分工具获取失败:', failedToolIds); + } + } + + // 3. 构建 stepsText(保持原有逻辑) + const stepsText = generatedSkillData.execution_plan.steps + .map((step, index) => { + let stepText = `步骤 ${index + 1}: ${step.title}\n${step.description}`; + if (step.expectedTools && step.expectedTools.length > 0) { + const tools = step.expectedTools + .map((tool) => `${tool.type === 'tool' ? '🔧' : '📚'} ${tool.id}`) + .join(', '); + stepText += `\n使用工具: ${tools}`; + } + return stepText; + }) + .join('\n\n'); + + // 4. 应用生成的数据,包含 selectedTools + onAIGenerate({ + name: generatedSkillData.plan_analysis.name || skill.name, + description: generatedSkillData.plan_analysis.description || skill.description, + stepsText: stepsText, + selectedTools: newTools + }); }} /> diff --git a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/utils.ts b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/utils.ts index f226b66db..3cbbcbba7 100644 --- a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/utils.ts +++ b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/utils.ts @@ -308,3 +308,51 @@ export const checkNeedsUserConfiguration = (toolTemplate: FlowNodeTemplateType): false ); }; + +/** + * Get the configuration status of a tool + * Checks if tool needs configuration and whether all required fields are filled + * @param toolTemplate - The tool template to check + * @returns 'active' if tool is ready to use, 'waitingForConfig' if configuration needed + */ +export const getToolConfigStatus = ( + toolTemplate: FlowNodeTemplateType +): 'active' | 'waitingForConfig' => { + // Check if tool needs configuration + const needsConfig = checkNeedsUserConfiguration(toolTemplate); + if (!needsConfig) { + return 'active'; + } + + // For tools that need config, check if all required fields have values + const formRenderTypesMap: Record = { + [FlowNodeInputTypeEnum.input]: true, + [FlowNodeInputTypeEnum.textarea]: true, + [FlowNodeInputTypeEnum.numberInput]: true, + [FlowNodeInputTypeEnum.password]: true, + [FlowNodeInputTypeEnum.switch]: true, + [FlowNodeInputTypeEnum.select]: true, + [FlowNodeInputTypeEnum.JSONEditor]: true, + [FlowNodeInputTypeEnum.timePointSelect]: true, + [FlowNodeInputTypeEnum.timeRangeSelect]: true + }; + + // Find all inputs that need configuration + const configInputs = toolTemplate.inputs.filter((input) => { + if (input.toolDescription) return false; + if (input.key === NodeInputKeyEnum.forbidStream) return false; + if (input.key === NodeInputKeyEnum.systemInputConfig) return true; + return input.renderTypeList.some((type) => formRenderTypesMap[type]); + }); + + // Check if all required fields are filled + const allConfigured = configInputs.every((input) => { + const value = input.value; + if (value === undefined || value === null || value === '') return false; + if (Array.isArray(value) && value.length === 0) return false; + if (typeof value === 'object' && Object.keys(value).length === 0) return false; + return true; + }); + + return allConfigured ? 'active' : 'waitingForConfig'; +}; diff --git a/projects/app/src/pages/api/core/ai/skill/detail.ts b/projects/app/src/pages/api/core/ai/skill/detail.ts index 50139b9b2..3267f42f1 100644 --- a/projects/app/src/pages/api/core/ai/skill/detail.ts +++ b/projects/app/src/pages/api/core/ai/skill/detail.ts @@ -48,9 +48,19 @@ async function handler( appId: tool.id, lang: getLocale(req) }); + + // Merge saved config back into inputs + const mergedInputs = toolNode.inputs.map((input) => ({ + ...input, + value: + tool.config && tool.config[input.key] !== undefined + ? tool.config[input.key] // Use saved config value + : input.value // Keep default value + })); + return { ...toolNode, - configStatus: 'active' as const + inputs: mergedInputs }; } catch (error) { // If tool not found or error, mark as invalid