diff --git a/packages/service/core/ai/llm/agentCall.ts b/packages/service/core/ai/llm/agentCall.ts index 6fe5d73c5eb7..18d7486e6238 100644 --- a/packages/service/core/ai/llm/agentCall.ts +++ b/packages/service/core/ai/llm/agentCall.ts @@ -14,10 +14,14 @@ import type { CreateLLMResponseProps, ResponseEvents } from './request'; import { createLLMResponse } from './request'; import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d'; import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; +import { countGptMessagesTokens } from '../../../common/string/tiktoken/index'; +import { addLog } from '../../../common/system/log'; +import type { AgentPlanStepType } from '../../workflow/dispatch/ai/agent/sub/plan/type'; type RunAgentCallProps = { maxRunAgentTimes: number; interactiveEntryToolParams?: WorkflowInteractiveResponseType['toolParams']; + currentStep?: AgentPlanStepType; body: { messages: ChatCompletionMessageParam[]; @@ -56,9 +60,260 @@ type RunAgentResponse = { subAppUsages: ChatNodeUsageType[]; }; +/** + * 压缩 Agent 对话历史 + * 当 messages 的 token 长度超过模型最大长度的 0.7 时,调用 LLM 进行压缩 + */ +const compressAgentMessages = async ( + messages: ChatCompletionMessageParam[], + model: LLMModelItemType, + currentDescription: string +): Promise => { + if (!messages || messages.length === 0) return messages; + + const tokenCount = await countGptMessagesTokens(messages); + const maxTokenThreshold = model.maxContext * 0.7; + // Test + // const maxTokenThreshold = 10000; + + addLog.debug('Agent messages token check', { + tokenCount, + maxTokenThreshold, + needCompress: tokenCount > maxTokenThreshold + }); + + const messagesJson = JSON.stringify(messages, null, 2); + + if (tokenCount <= maxTokenThreshold) { + console.log('messages 无需压缩,共', messages.length, '条消息'); + // console.log('messagesJson', messagesJson); + // messages.forEach((msg, idx) => { + // console.log(`\n=== Message ${idx} (${msg.role}) ===`); + // console.log(JSON.stringify(msg, null, 2)); + // }); + return messages; + } + + const compressionRatio = 0.6; + const targetTokens = Math.round(tokenCount * compressionRatio); + + addLog.info('Start compressing agent messages', { + originalTokens: tokenCount, + targetTokens, + compressionRatio + }); + + const systemPrompt = `你是 Agent 对话历史压缩专家。你的任务是将对话历史压缩到目标 token 数,同时确保工具调用的 ID 映射关系完全正确。 + + ## 当前任务目标 + ${currentDescription} + + ## 压缩目标(最高优先级) + - **原始 token 数**: ${tokenCount} tokens + - **目标 token 数**: ${targetTokens} tokens (压缩比例: ${Math.round(compressionRatio * 100)}%) + - **约束**: 输出的 JSON 内容必须接近 ${targetTokens} tokens + + --- + + ## 三阶段压缩工作流 + + ### 【第一阶段:扫描与标注】(内部思考,不输出) + + 在开始压缩前,请先在内心完成以下分析: + + 1. **构建 ID 映射表** + - 扫描所有 assistant 消息中的 tool_calls,提取每个 tool_call 的 id + - 找到对应的 tool 消息的 tool_call_id + - 建立一一对应的映射关系表,例如: + \`\`\` + call_abc123 → tool 消息 #5 + call_def456 → tool 消息 #7 + \`\`\` + + 2. **评估消息相关性** + 根据当前任务目标「${currentDescription}」,为每条消息标注相关性等级: + - **[高]**: 直接支撑任务目标,包含关键数据/结论 + - **[中]**: 间接相关,提供背景信息 + - **[低]**: 弱相关或无关,可大幅精简或删除 + + 3. **确定压缩策略** + - **system 消息**:保持完整,不做修改 + - 高相关消息:保留 70-90% 内容(精简冗余表达) + - 中等相关消息:保留 30-50% 内容(提炼核心要点) + - 低相关消息:保留 10-20% 内容或删除(仅保留一句话总结) + + --- + + ### 【第二阶段:执行压缩】 + + 基于第一阶段的分析,执行压缩操作: + + **压缩原则**: + 1. **ID 不可变**: 所有 tool_call 的 id 和 tool_call_id 必须原样保留,绝不修改 + 2. **结构完整**: 每个 tool_call 对象必须包含 \`id\`, \`type\`, \`function\` 字段 + 3. **顺序保持**: assistant 的 tool_calls 和对应的 tool 响应按原始顺序出现 + 4. **大幅精简 content**: + - tool 消息的 content:删除冗长描述、重复信息,只保留核心结论和关键数据 + - 合并相似的工具结果(但保留各自的 tool_call_id) + 5. **目标优先**: 围绕任务目标压缩,与目标无关的消息可删除 + + **压缩技巧**: + - 删除:详细过程描述、重复信息、失败尝试、调试日志 + - 保留:具体数据、关键结论、错误信息、链接引用 + - 精简:用"核心发现:A、B、C"代替长篇叙述 + + --- + + ### 【第三阶段:自校验】 + + 输出前,必须检查: + + 1. **ID 一致性校验** + - 每个 assistant 消息中的 tool_calls[i].id 是否有对应的 tool 消息? + - 每个 tool 消息的 tool_call_id 是否能在前面的 assistant 消息中找到? + - 是否所有 ID 都原样保留,没有修改或生成新 ID? + + 2. **压缩比例校验** + - 估算输出的 JSON 字符串长度,是否接近 ${targetTokens} tokens? + - 如果超出目标,需进一步精简 content 字段 + + 3. **格式完整性校验** + - 所有 tool_call 对象是否包含完整的 \`id\`, \`type\`, \`function\` 字段? + - JSON 结构是否正确? + + --- + + ## 输出格式 + + 请按照以下 JSON 格式输出(必须使用 \`\`\`json 代码块): + + \`\`\`json + { + "compressed_messages": [ + {"role": "system", "content": "系统指令(精简后)"}, + {"role": "user", "content": "用户请求"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_原始ID", + "type": "function", + "function": { + "name": "工具名", + "arguments": "{\\"param\\":\\"精简后的值\\"}" + } + } + ] + }, + { + "role": "tool", + "tool_call_id": "call_原始ID", + "content": "工具返回的核心结果(已大幅精简,只保留关键信息)" + } + ], + "compression_summary": "原始${tokenCount}tokens → 约X tokens (压缩比例Y%)。操作:删除了Z条低相关消息,精简了N个工具响应。ID映射关系已验证正确。" + } + \`\`\` + + --- + + ## 压缩示例 + + **示例 1:工具调用压缩** + + 原始(500+ tokens): + \`\`\`json + [ + {"role": "assistant", "tool_calls": [{"id": "call_abc", "type": "function", "function": {"name": "search", "arguments": "{\\"query\\":\\"Python性能优化完整指南\\",\\"max_results\\":10}"}}]}, + {"role": "tool", "tool_call_id": "call_abc", "content": "找到10篇文章:\\n1. 标题:Python性能优化完整指南\\n 作者:张三\\n 发布时间:2024-01-15\\n 摘要:本文详细介绍了Python性能优化的各种技巧,包括...(此处省略400字详细内容)\\n URL: https://example.com/article1\\n2. 标题:..."} + ] + \`\`\` + + 压缩后(100 tokens): + \`\`\`json + [ + {"role": "assistant", "tool_calls": [{"id": "call_abc", "type": "function", "function": {"name": "search", "arguments": "{\\"query\\":\\"Python性能优化\\"}"}}]}, + {"role": "tool", "tool_call_id": "call_abc", "content": "找到10篇文章。核心发现:①Cython可提升30%性能 ②NumPy向量化比循环快10倍 ③使用__slots__节省内存"} + ] + \`\`\` + + **示例 2:相似内容合并** + + 如果有多个相似的搜索结果,可以合并 content,但必须保留各自的 ID 映射。 + + --- + + ## 待压缩的对话历史 + + ${messagesJson} + + --- + + 请严格按照三阶段工作流执行,确保 ID 映射关系完全正确,输出接近目标 token 数。`; + + const userPrompt = '请执行压缩操作,严格按照JSON格式返回结果。'; + + try { + const { answerText } = await createLLMResponse({ + body: { + model, + messages: [ + { + role: ChatCompletionRequestMessageRoleEnum.System, + content: systemPrompt + }, + { + role: ChatCompletionRequestMessageRoleEnum.User, + content: userPrompt + } + ], + temperature: 0.1, + stream: false + } + }); + + if (!answerText) { + addLog.warn('Compression failed: empty response, return original messages'); + return messages; + } + + const jsonMatch = + answerText.match(/```json\s*([\s\S]*?)\s*```/) || answerText.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + addLog.warn('Compression failed: cannot parse JSON, return original messages'); + return messages; + } + + const jsonText = jsonMatch[1] || jsonMatch[0]; + const parsed = JSON.parse(jsonText); + + if (!parsed.compressed_messages || !Array.isArray(parsed.compressed_messages)) { + addLog.warn('Compression failed: invalid format, return original messages'); + return messages; + } + + const compressedTokens = await countGptMessagesTokens(parsed.compressed_messages); + addLog.info('Agent messages compressed successfully', { + originalTokens: tokenCount, + compressedTokens, + actualRatio: (compressedTokens / tokenCount).toFixed(2), + summary: parsed.compression_summary + }); + + // console.log("------------- \n压缩完成 \n压缩前的 message:", messagesJson); + // console.log('压缩后的 message:', JSON.stringify(parsed.compressed_messages, null, 2)); + return parsed.compressed_messages as ChatCompletionMessageParam[]; + } catch (error) { + addLog.error('Compression failed', error); + return messages; + } +}; + export const runAgentCall = async ({ maxRunAgentTimes, interactiveEntryToolParams, + currentStep, body: { model, messages, stream, temperature, top_p, subApps }, userKey, isAborted, @@ -123,8 +378,27 @@ export const runAgentCall = async ({ const requestMessagesLength = requestMessages.length; requestMessages = completeMessages.slice(); - // Tool run and concat messages + let isFirstTool = true; + console.log('toolCalls', toolCalls); for await (const tool of toolCalls) { + console.log('tool', tool); + if (isFirstTool) { + const lastMessage = requestMessages[requestMessages.length - 1]; + if (lastMessage?.role === ChatCompletionRequestMessageRoleEnum.Assistant) { + requestMessages[requestMessages.length - 1] = { + role: ChatCompletionRequestMessageRoleEnum.Assistant, + content: lastMessage.content || '', + tool_calls: [tool] + }; + } + isFirstTool = false; + } else { + requestMessages.push({ + role: ChatCompletionRequestMessageRoleEnum.Assistant, + content: '', + tool_calls: [tool] + }); + } // TODO: 加入交互节点处理 const { response, usages, interactive } = await handleToolResponse({ call: tool, @@ -136,6 +410,13 @@ export const runAgentCall = async ({ role: ChatCompletionRequestMessageRoleEnum.Tool, content: response }); + if (currentStep) { + const taskDescription = currentStep.description || currentStep.title; + if (taskDescription) { + requestMessages = await compressAgentMessages(requestMessages, model, taskDescription); + } + } + subAppUsages.push(...usages); if (interactive) { diff --git a/packages/service/core/workflow/dispatch/ai/agent/constants.ts b/packages/service/core/workflow/dispatch/ai/agent/constants.ts index 1083b0750a1c..d8c6621e1dee 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/constants.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/constants.ts @@ -1,47 +1,193 @@ import type { AgentPlanStepType } from './sub/plan/type'; import type { AgentPlanType } from './sub/plan/type'; +import { getLLMModel } from '../../../../ai/model'; +import { countPromptTokens } from '../../../../../common/string/tiktoken/index'; +import { createLLMResponse } from '../../../../ai/llm/request'; +import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; +import { addLog } from '../../../../../common/system/log'; -export const getMasterAgentSystemPrompt = ({ +/** + * 压缩步骤提示词 + * 当 stepPrompt 的 token 长度超过模型最大长度的 0.7 时,调用 LLM 进行压缩 + */ +const compressStepPrompt = async ( + stepPrompt: string, + model: string, + currentDescription: string +): Promise => { + if (!stepPrompt) return stepPrompt; + + const modelData = getLLMModel(model); + if (!modelData) return stepPrompt; + + const tokenCount = await countPromptTokens(stepPrompt); + const maxTokenThreshold = modelData.maxContext * 0.7; + + if (tokenCount <= maxTokenThreshold) { + return stepPrompt; + } + + const compressionRatio = 0.6; + + const compressionSystemPrompt = ` +你是工作流步骤历史压缩专家,擅长从多个已执行步骤的结果中提取关键信息。 +你的任务是对工作流的执行历史进行智能压缩,在保留关键信息的同时,大幅降低 token 消耗。 + + + + 输入内容是按照"步骤ID → 步骤标题 → 执行结果"格式组织的多个步骤记录。 + 你需要根据当前任务目标,对这些历史记录进行分级压缩。 + + + + **第一阶段:快速扫描与相关性评估** + + 在开始压缩前,请先在内心完成以下思考(不需要输出): + 1. 浏览所有步骤,识别每个步骤与当前任务目标的相关性 + 2. 为每个步骤标注相关性等级: + - [高]:直接支撑当前任务,包含关键数据或结论 + - [中]:间接相关,提供背景信息或辅助判断 + - [低]:弱相关或无关,可大幅精简或省略 + 3. 确定压缩策略:基于相关性等级,决定每个步骤的保留程度 + + **第二阶段:执行分级压缩** + + 根据第一阶段的评估,按以下策略压缩: + + 1. **高相关步骤**(保留度 80-100%) + - 完整保留:步骤ID、标题、核心执行结果 + - 保留所有:具体数据、关键结论、链接引用、重要发现 + - 仅精简:去除啰嗦的过程描述和冗余表达 + + 2. **中等相关步骤**(保留度 40-60%) + - 保留:步骤ID、标题、核心要点 + - 提炼:将执行结果浓缩为 2-3 句话 + - 去除:详细过程、重复信息、次要细节 + + 3. **低相关步骤**(保留度 10-20%) + - 保留:步骤ID、标题 + - 极简化:一句话总结(或直接省略执行结果) + - 判断:如果完全无关,可整体省略该步骤 + + + + - 删繁就简:移除重复、冗长的描述性内容 + - 去粗取精:针对当前任务目标,保留最相关的要素 + - 保数据留结论:优先保留具体数据、关键结论、链接引用 + - 保持时序:按原始顺序输出,不要打乱逻辑 + - 可追溯性:保留必要的步骤标识,确保能理解信息来源 + - 识别共性:如果连续多个步骤结果相似,可合并描述 + + + + 压缩完成后,请自我检查: + 1. 是否达到了目标压缩比例? + 2. 当前任务所需的关键信息是否都保留了? + 3. 压缩后的内容是否仍能让后续步骤理解发生了什么? + 4. 步骤的时序关系是否清晰? + `; + + const userPrompt = `请对以下工作流步骤的执行历史进行压缩,保留与当前任务最相关的信息。 + +**当前任务目标**:${currentDescription} + +**需要压缩的步骤历史**: +${stepPrompt} + +**目标压缩比例**:${Math.round(compressionRatio * 100)}%(目标长度为原文的${Math.round(compressionRatio * 100)}%) + +**输出格式要求**: +1. 保留步骤结构:每个步骤使用"# 步骤ID: [id]\\n\\t - 步骤标题: [title]\\n\\t - 执行结果: [精简后的结果]"的格式 +2. 根据相关性分级处理: + - 与当前任务高度相关的步骤:保留完整的关键信息(数据、结论、链接等) + - 中等相关的步骤:提炼要点,移除冗余描述 + - 低相关的步骤:仅保留一句话总结或省略执行结果 +3. 保持步骤顺序:按原始顺序输出,不要打乱 +4. 提取共性:如果连续多个步骤结果相似,可以适当合并描述 + +**质量标准**: +- 压缩后的内容能让后续步骤理解前置步骤做了什么、得到了什么结果 +- 保留所有对当前任务有价值的具体数据和关键结论 +- 移除重复、啰嗦的描述性文字 + +请直接输出压缩后的步骤历史:`; + + try { + const { answerText } = await createLLMResponse({ + body: { + model: modelData, + messages: [ + { + role: ChatCompletionRequestMessageRoleEnum.System, + content: compressionSystemPrompt + }, + { + role: ChatCompletionRequestMessageRoleEnum.User, + content: userPrompt + } + ], + temperature: 0.1, + stream: false + } + }); + + return answerText || stepPrompt; + } catch (error) { + console.error('压缩 stepPrompt 失败:', error); + // 压缩失败时返回原始内容 + return stepPrompt; + } +}; + +export const getMasterAgentSystemPrompt = async ({ steps, step, userInput, - background = '' + background = '', + model }: { steps: AgentPlanStepType[]; step: AgentPlanStepType; userInput: string; background?: string; + model: string; }) => { - const stepPrompt = steps + let stepPrompt = steps .filter((item) => step.depends_on && step.depends_on.includes(item.id)) - .map((item) => `-步骤ID: ${item.id}\n\t步骤标题: ${item.title}\n\t执行结果: ${item.response}`) + .map( + (item) => + `# 步骤ID: ${item.id}\n\t - 步骤标题: ${item.title}\n\t - 执行结果: ${item.response}` + ) .filter(Boolean) .join('\n'); + addLog.debug(`Step call depends_on (LLM): ${step.id}`, step.depends_on); + // 压缩依赖的上下文 + stepPrompt = await compressStepPrompt(stepPrompt, model, step.description || step.title); return `请根据任务背景、之前步骤的执行结果和当前步骤要求选择并调用相应的工具。如果是一个总结性质的步骤,请整合之前步骤的结果进行总结。 -【任务背景】 -目标: ${userInput} -前置信息: ${background} - -【当前步骤】 -步骤ID: ${step.id} -步骤标题: ${step.title} - -${ - stepPrompt - ? `【之前步骤的执行结果】 -${stepPrompt}` - : '' -} - -【执行指导】 -1. 仔细阅读前面步骤的执行结果,理解已经获得的信息 -2. 根据当前步骤描述和前面的结果,分析需要使用的工具 -3. 从可用工具列表中选择最合适的工具 -4. 基于前面步骤的结果为工具生成合理的参数 -5. 如果需要多个工具,可以同时调用 -6. 确保当前步骤的执行能够有效利用和整合前面的结果 -7. 如果是总结的步骤,请利用之前步骤的信息进行全面总结 - -请严格按照步骤描述执行,确保完成所有要求的子任务。`; + 【任务背景】 + 目标: ${userInput} + 前置信息: ${background} + + 【当前步骤】 + 步骤ID: ${step.id} + 步骤标题: ${step.title} + + ${ + stepPrompt + ? `【之前步骤的执行结果】 + ${stepPrompt}` + : '' + } + + 【执行指导】 + 1. 仔细阅读前面步骤的执行结果,理解已经获得的信息 + 2. 根据当前步骤描述和前面的结果,分析需要使用的工具 + 3. 从可用工具列表中选择最合适的工具 + 4. 基于前面步骤的结果为工具生成合理的参数 + 5. 如果需要多个工具,可以同时调用 + 6. 确保当前步骤的执行能够有效利用和整合前面的结果 + 7. 如果是总结的步骤,请利用之前步骤的信息进行全面总结 + + 请严格按照步骤描述执行,确保完成所有要求的子任务。`; }; diff --git a/packages/service/core/workflow/dispatch/ai/agent/index.ts b/packages/service/core/workflow/dispatch/ai/agent/index.ts index 8d848d639a6f..808283370634 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/index.ts @@ -30,6 +30,8 @@ import type { localeType } from '@fastgpt/global/common/i18n/type'; import { stepCall } from './master/call'; import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; import { addLog } from '../../../../../common/system/log'; +import { createLLMResponse } from '../../../../ai/llm/request'; +import { parseToolArgs } from '../utils'; export type DispatchAgentModuleProps = ModuleDispatchProps<{ [NodeInputKeyEnum.history]?: ChatItemType[]; @@ -255,10 +257,115 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise } }; + /** + * 检测问题复杂度 + * @returns true: 复杂问题,需要正常规划流程; false: 简单问题,已构造简单 plan + */ + const checkQuestionComplexity = async (): Promise => { + addLog.debug('Checking if question is simple...'); + + const simpleCheckPrompt = `你是一位资深的认知复杂度评估专家 (Cognitive Complexity Assessment Specialist)。 您的职责是对用户提出的任务请求进行深度解析,精准判断其内在的认知复杂度层级,并据此决定是否需要启动多步骤规划流程。 + +用户显式意图 (User Explicit Intent): +用户可能会在问题中明确表达其期望的回答方式或处理深度。 常见的意图类型包括: +* **快速回答 / 简单回答 (Quick/Simple Answer)**:用户期望得到简洁、直接的答案,无需深入分析或详细解释。 例如:“请简单回答...”、“快速告诉我...” +* **深度思考 / 详细分析 (Deep Thinking/Detailed Analysis)**:用户期望得到深入、全面的分析,包括多角度的思考、证据支持和详细的解释。 例如:“请深入分析...”、“详细解释...” +* **创造性方案 / 创新性建议 (Creative Solution/Innovative Suggestion)**:用户期望得到具有创新性的解决方案或建议,可能需要进行发散性思维和方案设计。 例如:“请提出一个创新的方案...”、“提供一些有创意的建议...” +* **无明确意图 (No Explicit Intent)**:用户没有明确表达其期望的回答方式或处理深度。 + +评估框架 (Assessment Framework): +* **低复杂度任务 (Low Complexity - \`complex: false\`)**: 此类任务具备高度的直接性和明确性,通常仅需调用单一工具或执行简单的操作即可完成。 其特征包括: + * **直接工具可解性 (Direct Tool Solvability)**:任务目标明确,可直接映射到特定的工具功能。 + * **信息可得性 (Information Accessibility)**:所需信息易于获取,无需复杂的搜索或推理。 + * **操作单一性 (Operational Singularity)**:任务执行路径清晰,无需多步骤协同。 + * **典型示例 (Typical Examples)**:信息检索 (Information Retrieval)、简单算术计算 (Simple Arithmetic Calculation)、事实性问题解答 (Factual Question Answering)、目标明确的单一指令执行 (Single, Well-Defined Instruction Execution)。 +* **高复杂度任务 (High Complexity - \'complex: true\')**: 此类任务涉及复杂的认知过程,需要进行多步骤规划、工具组合、深入分析和创造性思考才能完成。 其特征包括: + * **意图模糊性 (Intent Ambiguity)**:用户意图不明确,需要进行意图消歧 (Intent Disambiguation) 或目标细化 (Goal Refinement)。 + * **信息聚合需求 (Information Aggregation Requirement)**:需要整合来自多个信息源的数据,进行综合分析。 + * **推理与判断 (Reasoning and Judgement)**:需要进行逻辑推理、情境分析、价值判断等认知操作。 + * **创造性与探索性 (Creativity and Exploration)**:需要进行发散性思维、方案设计、假设验证等探索性活动。 + * ** + * **典型示例 (Typical Examples)**:意图不明确的请求 (Ambiguous Requests)、需要综合多个信息源的任务 (Tasks Requiring Information Synthesis from Multiple Sources)、需要复杂推理或创造性思考的问题 (Problems Requiring Complex Reasoning or Creative Thinking)。 +待评估用户问题 (User Query): ${userChatInput} + +输出规范 (Output Specification): +请严格遵循以下 JSON 格式输出您的评估结果: +\`\`\`json +{ + "complex": true/false, + "reason": "对任务认知复杂度的详细解释,说明判断的理由,并引用上述评估框架中的相关概念。" +} +\`\`\` + +`; + + try { + const { answerText: checkResult } = await createLLMResponse({ + body: { + model: agentModel.model, + temperature: 0.1, + messages: [ + { + role: 'system', + content: simpleCheckPrompt + }, + { + role: 'user', + content: userChatInput + } + ] + } + }); + + const checkResponse = parseToolArgs<{ complex: boolean; reason: string }>(checkResult); + + if (checkResponse && !checkResponse.complex) { + // 构造一个简单的 plan,包含一个直接回答的 step + agentPlan = { + task: userChatInput, + steps: [ + { + id: 'Simple-Answer', + title: '回答问题', + description: `直接回答用户问题:${userChatInput}`, + response: undefined + } + ], + replan: false + }; + + workflowStreamResponse?.({ + event: SseResponseEventEnum.answer, + data: textAdaptGptResponse({ + text: `检测到简单问题,直接回答中...\n` + }) + }); + + return false; // 简单问题 + } else { + return true; // 复杂问题 + } + } catch (error) { + addLog.error('Simple question check failed, proceeding with normal plan flow', error); + return true; // 出错时默认走复杂流程 + } + }; + /* ===== Plan Agent ===== */ if (isPlanStep) { - const result = await planCallFn(); - if (result) return result; + // 如果是用户确认 plan 的交互,直接调用 planCallFn,不需要再检测复杂度 + if (lastInteractive?.type === 'agentPlanCheck' && interactiveInput === ConfirmPlanAgentText) { + const result = await planCallFn(); + if (result) return result; + } else { + // 非交互确认的情况下,先检测问题复杂度 + const isComplex = await checkQuestionComplexity(); + + if (isComplex) { + const result = await planCallFn(); + if (result) return result; + } + } } else if (isReplanStep) { const result = await replanCallFn({ plan: agentPlan! @@ -281,6 +388,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise while (agentPlan?.steps!.filter((item) => !item.response)!.length) { const pendingSteps = agentPlan?.steps!.filter((item) => !item.response)!; + for await (const step of pendingSteps) { addLog.debug(`Step call: ${step.id}`, step); diff --git a/packages/service/core/workflow/dispatch/ai/agent/master/call.ts b/packages/service/core/workflow/dispatch/ai/agent/master/call.ts index 7c1d0d1f0238..0249c41886ff 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/master/call.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/master/call.ts @@ -98,8 +98,16 @@ export const stepCall = async ({ step }); step.depends_on = depends; - addLog.debug(`Step call depends_on (LLM): ${step.id}`, depends); - addLog.debug(`Step information`, steps); + + // addLog.debug(`Step information`, steps); + const systemPromptContent = await getMasterAgentSystemPrompt({ + steps, + step, + userInput: userChatInput, + model + // background: systemPrompt + }); + const requestMessages = chats2GPTMessages({ messages: [ { @@ -107,12 +115,7 @@ export const stepCall = async ({ value: [ { text: { - content: getMasterAgentSystemPrompt({ - steps, - step, - userInput: userChatInput - // background: systemPrompt - }) + content: systemPromptContent } } ] @@ -134,6 +137,7 @@ export const stepCall = async ({ const { assistantResponses, inputTokens, outputTokens, subAppUsages, interactiveResponse } = await runAgentCall({ maxRunAgentTimes: 100, + currentStep: step, // interactiveEntryToolParams: lastInteractive?.toolParams, body: { messages: requestMessages, 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 6c32094bd85d..41023420e95d 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 @@ -21,6 +21,7 @@ import { PlanCheckInteractive } from './constants'; import type { AgentPlanType } from './type'; import type { GetSubAppInfoFnType } from '../../type'; import { getStepDependon } from '../../common/dependon'; +import { parseSystemPrompt } from '../../utils'; type PlanAgentConfig = { systemPrompt?: string; @@ -70,8 +71,7 @@ export const dispatchPlanAgent = async ({ role: 'system', content: getPlanAgentSystemPrompt({ getSubAppInfo, - subAppList, - systemPrompt + subAppList }) }, ...historyMessages @@ -95,9 +95,15 @@ export const dispatchPlanAgent = async ({ content: '请基于以上收集的用户信息,重新生成完整的计划,严格按照 JSON Schema 输出。' }); } else { + let userContent = `任务描述:${userInput}`; + + if (systemPrompt) { + userContent += `\n\n背景信息:${parseSystemPrompt({ systemPrompt, getSubAppInfo })}\n请按照用户提供的背景信息来重新生成计划,优先遵循用户的步骤安排和偏好。`; + } + console.log('userContent:', userInput); requestMessages.push({ role: 'user', - content: userInput + content: userContent }); } @@ -212,7 +218,7 @@ export const dispatchReplanAgent = async ({ userInput, plan, background, - referencePlans, + systemPrompt, model, temperature, @@ -271,7 +277,7 @@ export const dispatchReplanAgent = async ({ task: userInput, dependsSteps: replanSteps, background, - referencePlans + systemPrompt: parseSystemPrompt({ systemPrompt, getSubAppInfo }) }) }); } 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 502358879d02..b952cc07f25e 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 @@ -24,16 +24,12 @@ const getSubAppPrompt = ({ export const getPlanAgentSystemPrompt = ({ getSubAppInfo, - subAppList, - systemPrompt + subAppList }: { getSubAppInfo: GetSubAppInfoFnType; subAppList: ChatCompletionTool[]; - systemPrompt?: string; }) => { - const userSystemPrompt = parseSystemPrompt({ systemPrompt, getSubAppInfo }); const subAppPrompt = getSubAppPrompt({ getSubAppInfo, subAppList }); - console.log(userSystemPrompt); return ` 你是一个专业的主题计划构建专家,擅长将复杂的主题学习和探索过程转化为结构清晰、可执行的渐进式学习路径。你的规划方法强调: @@ -45,14 +41,14 @@ export const getPlanAgentSystemPrompt = ({ 1. **渐进式规划**:只规划到下一个关键信息点或决策点,通过 'replan' 标识需要基于执行结果调整的任务节点 2. **最小化假设**:不对未知信息做过多预设,而是通过执行步骤获取 - 3. **前置信息优先**:制定计划前,优先收集必要的前置信息,而不是将信息收集作为计划的一部分 + 3. **前置信息优先**:制定计划前,优先收集必要的前置信息,而不是将信息收集作为计划的一部分,如果用户提供的 PLAN 中有前置搜集工作请在规划之前搜集 4. **格式限制**:所有输出的信息必须输出符合 JSON Schema 的格式 5. **目标强化**:所有的任务信息必须要规划出一个 PLAN 「以下是在规划 PLAN 过程中可以使用在每个 step 的 description 中的工具」 ${subAppPrompt} - 「以下是在规划 PLAN 过程中可以用来调用的工具」 + 「以下是在规划 PLAN 过程中可以用来调用的工具,不应该在 step 的 description 中」 - [@${SubAppIds.ask}]:${PlanAgentAskTool.function.description} @@ -254,46 +250,77 @@ export const getReplanAgentSystemPrompt = ({ }) => { const subAppPrompt = getSubAppPrompt({ getSubAppInfo, subAppList }); - return ` - 你是一个智能流程优化专家,专门负责在已完成的任务步骤基础上,追加生成优化步骤来完善整个流程,确保任务目标的完美达成。 - 你的任务不是重新规划,而是基于现有执行结果,识别可以进一步优化和完善的环节,并生成具体的追加步骤,如果现有的结果已经可以实现当前的目标可以不用进行重新规划,直接输出总结。 - - - 核心原则: - 1. **追加优化**:在现有步骤基础上增加新步骤,不修改已完成的工作 - 2. **结果导向**:基于实际执行结果,识别需要进一步完善的方面 - 3. **价值最大化**:确保每个新步骤都能为整体目标提供实际价值 - 4. **流程闭环**:补充遗漏的环节,形成完整的任务闭环 - 5. **任务核查**:确保最终输出的步骤能够完整覆盖用户最初提出的任务目标 - + return ` + 你是一个智能流程优化专家,专门负责在已完成的任务步骤基础上,追加生成优化步骤来完善整个流程,确保任务目标的完美达成。 + 你的任务不是重新规划,而是基于现有执行结果和任务类型,决定是输出总结还是继续生成优化步骤。 + + + 核心原则: + 1. **任务类型识别**:区分确定性任务(Deterministic Task)和探究性任务(Exploratory Task) + 2. **追加优化**:在现有步骤基础上增加新步骤,不修改已完成的工作 + 3. **结果导向**:基于实际执行结果,识别需要进一步完善的方面 + 4. **价值最大化**:确保每个新步骤都能为整体目标提供实际价值 + 5. **流程闭环**:补充遗漏的环节,形成完整的任务闭环 + 6. **任务核查**:确保最终输出的步骤能够完整覆盖用户最初提出的任务目标 + + + + 1. **确定性任务(Deterministic Task)**: + - 特征:有明确的答案或结论,问题边界清晰 + - 示例:查询天气、查找特定信息、计算数值、回答事实性问题、解决明确定义的问题 + - 策略:如果已有信息足以给出准确答案,直接输出总结步骤 + + 2. **探究性任务(Exploratory Task)**: + - 特征:需要深入探索、多维度分析、创造性规划,答案越详细越好 + - 示例:制定旅游计划、设计解决方案、学习某个主题、评估多个选项、创作内容、规划项目 + - 策略:即使已有一些结果,也应该生成更详细的优化步骤,追求全面性和深度 + +「以下是在规划 PLAN 过程中可以使用在每个 step 的 description 中的工具」 ${subAppPrompt} +「以下是在规划 PLAN 过程中可以用来调用的工具,不应该在 step 的 description 中」 +- [@${SubAppIds.ask}]:${PlanAgentAskTool.function.description} - - 1. **完整性评估:** - * 审视「关键步骤执行结果」及其「执行结果」。 + + 1. **任务类型识别:** + * 首先判断「任务目标」属于哪种类型: + * **确定性任务**:是否是查询特定信息、回答事实问题、计算、查找等明确答案的任务? + * **探究性任务**:是否需要规划、设计、学习、评估、创作等深入探索的任务? + * 记住这个判断,它将影响后续的决策 + + 2. **完整性评估:** + * 审视「关键步骤执行结果」及其「执行结果」 * 深度思考: * (a) 基于现有的信息,是否能够对用户最初提出的「任务目标」,给出一个准确、完整、且具有实践指导意义的【最终结论】? * (b) 是否存在任何潜在的风险、遗漏的信息、或未充分考虑的因素,可能导致【最终结论】不够可靠或有效? - * 评估结果: - * 如果(a)为【是】,且(b)为【否】,则进入【总结步骤】。 - * 否则,进入【优化步骤】。 - - 2. **优化步骤 (当完整性评估为“否”时执行):** + * 结合任务类型做出决策: + * **确定性任务**:如果(a)为【是】且(b)为【否】,直接进入【总结步骤】 + * **探究性任务**:即使(a)为【是】,也要考虑是否可以通过更多步骤提供更全面、更深入的结果。只有当已有信息非常充分、全面时才进入【总结步骤】,否则进入【优化步骤】生成更详细的规划 + + 3. **优化步骤 (当需要继续优化时执行):** * 识别需要进一步优化和完善的环节: - * 针对「关键步骤执行结果」的不足之处,明确指出需要补充的信息、需要重新审视的假设、或者需要进一步探索的方向。 + * 针对「关键步骤执行结果」的不足之处,明确指出需要补充的信息、需要重新审视的假设、或者需要进一步探索的方向 + * **对于探究性任务**:即使现有结果不错,也考虑如何让答案更全面、更详细、更有价值 + * 前置信息检查: + * 首先判断是否具备制定计划所需的所有关键信息 + * 如果缺少用户偏好、具体场景细节、关键约束、目标参数等前置信息 + * **立即调用 ${SubAppIds.ask} 工具**,提出清晰的问题列表收集信息 + * **切记**:不要将"询问用户"、"收集信息"作为计划的步骤 * 生成具体的追加步骤: - * 基于上述识别结果,设计清晰、可操作的后续行动步骤,确保每个步骤都能够有效地弥补已发现的不足,并将流程导向更完善的状态。 - * 确保新步骤与已有工作形成有机整体 - - 3. **总结步骤 (当完整性评估为“是”时执行):** - * 对「关键步骤执行结果」及其【最终结论】进行高度概括和提炼。 - * 强调流程中的关键决策点、核心发现、以及最具价值的实践经验。 - * 输出一步step为总结性质的步骤要求. 步骤标题为 \`生成总结报告\` 步骤描述为 \`基于现有步骤的结果,生成一个总结报告\` - - **所有输出严格遵循 JSON Schema 格式的追加优化步骤** + * 基于上述识别结果,设计清晰、可操作的后续行动步骤 + * 确保新步骤与已有工作形成有机整体 + * **对于探究性任务**:追求深度和广度,生成多个维度的优化步骤 + + 4. **总结步骤 (当可以总结时执行):** + * **确定性任务**:如果已有足够信息可以给出准确答案 + * **探究性任务**:如果已经进行了充分的多轮探索,信息已经非常全面和详细 + * 输出格式: + * 步骤标题为 \`生成总结报告\` + * 步骤描述为 \`基于现有步骤的结果,生成一个总结报告\` + + **所有输出严格遵循 JSON Schema 格式的追加优化步骤** - 必须严格输出 JSON 格式 - 生成的是**追加步骤**,用于在现有工作基础上进一步优化 @@ -417,14 +444,15 @@ ${subAppPrompt} export const getReplanAgentUserPrompt = ({ task, background, - referencePlans, + systemPrompt, dependsSteps }: { task: string; background?: string; - referencePlans?: string; + systemPrompt?: string; dependsSteps: AgentPlanStepType[]; }) => { + console.log('replan systemPrompt:', systemPrompt); const stepsResponsePrompt = dependsSteps .map( (step) => `步骤 ${step.id}: @@ -438,10 +466,9 @@ export const getReplanAgentUserPrompt = ({ ${background ? `「背景信息」:${background}` : ''} ${ - referencePlans + systemPrompt ? `「用户前置规划」: - ${referencePlans} - 请按照用户的前置规划来重新生成计划,优先遵循用户的步骤安排和偏好。` + ${systemPrompt}` : '' } @@ -451,6 +478,6 @@ export const getReplanAgentUserPrompt = ({ ${stepsResponsePrompt} - 请基于上述关键步骤 ${stepsIdPrompt} 的执行结果,生成能够进一步优化和完善整个任务目标的追加步骤。 + 请基于上述关键步骤 ${stepsIdPrompt} 的执行结果,生成能够进一步优化和完善整个任务目标的追加步骤,如果有「用户前置规划」请按照用户的前置规划来重新生成计划,优先遵循用户的步骤安排和偏好。。 如果「关键步骤执行结果」已经满足了当前的「任务目标」,请直接返回一个总结的步骤来提取最终的答案,而不需要进行其他的讨论`; };