From 249fa3e30d566ebb41b44c3fb05df9d464f0e95e Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Mon, 29 Dec 2025 13:26:58 +0800 Subject: [PATCH] agent usage --- .../service/core/ai/llm/agentCall/index.ts | 28 +- .../workflow/dispatch/ai/agent/constants.ts | 56 ++- .../workflow/dispatch/ai/agent/master/call.ts | 459 +++++++++--------- .../dispatch/ai/agent/master/dependon.ts | 6 +- .../dispatch/ai/agent/sub/plan/index.ts | 23 +- .../dispatch/ai/agent/sub/tool/index.ts | 5 +- 6 files changed, 320 insertions(+), 257 deletions(-) diff --git a/packages/service/core/ai/llm/agentCall/index.ts b/packages/service/core/ai/llm/agentCall/index.ts index e82562d145..da997dc2d0 100644 --- a/packages/service/core/ai/llm/agentCall/index.ts +++ b/packages/service/core/ai/llm/agentCall/index.ts @@ -17,6 +17,7 @@ import { computedMaxToken } from '../../utils'; import { filterGPTMessageByMaxContext } from '../utils'; import { getLLMModel } from '../../model'; import { filterEmptyAssistantMessages } from './utils'; +import { formatModelChars2Points } from '../../../../support/wallet/usage/utils'; type RunAgentCallProps = { maxRunAgentTimes: number; @@ -30,6 +31,7 @@ type RunAgentCallProps = { stream?: boolean; }; + usagePush: (usages: ChatNodeUsageType[]) => void; userKey?: CreateLLMResponseProps['userKey']; isAborted?: CreateLLMResponseProps['isAborted']; @@ -85,6 +87,8 @@ type RunAgentResponse = { export const runAgentCall = async ({ maxRunAgentTimes, body: { model, messages, max_tokens, tools, ...body }, + + usagePush, userKey, isAborted, @@ -159,6 +163,7 @@ export const runAgentCall = async ({ // 只需要推送本轮产生的 assistantMessages assistantMessages.push(...filterEmptyAssistantMessages(toolAssistantMessages)); subAppUsages.push(...usages); + usagePush(usages); // 相同 tool 触发了多次交互, 调用的 toolId 认为是相同的 if (interactive) { @@ -247,6 +252,24 @@ export const runAgentCall = async ({ consecutiveRequestToolTimes = 0; } + // Record usage + inputTokens += usage.inputTokens; + outputTokens += usage.outputTokens; + const { totalPoints, modelName } = formatModelChars2Points({ + model: modelData.model, + inputTokens: usage.inputTokens, + outputTokens: usage.outputTokens + }); + usagePush([ + { + moduleName: 'Agent 调用', + model: modelName, + totalPoints, + inputTokens: usage.inputTokens, + outputTokens: usage.outputTokens + } + ]); + // 3. 更新 messages const cloneRequestMessages = requestMessages.slice(); // 推送 AI 生成后的 assistantMessages @@ -278,6 +301,7 @@ export const runAgentCall = async ({ assistantMessages.push(...filterEmptyAssistantMessages(toolAssistantMessages)); // 因为 toolAssistantMessages 也需要记录成 AI 响应,所以这里需要推送。 subAppUsages.push(...usages); + usagePush(usages); if (interactive) { interactiveResponse = { @@ -296,10 +320,6 @@ export const runAgentCall = async ({ } } - // 6 Record usage - inputTokens += usage.inputTokens; - outputTokens += usage.outputTokens; - if (toolCalls.length === 0 || !!interactiveResponse || toolCallStep) { break; } diff --git a/packages/service/core/workflow/dispatch/ai/agent/constants.ts b/packages/service/core/workflow/dispatch/ai/agent/constants.ts index 91ef5b6b20..6e45925957 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/constants.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/constants.ts @@ -1,10 +1,12 @@ -import type { AgentPlanStepType } from './sub/plan/type'; +import type { AgentStepItemType } from '@fastgpt/global/core/ai/agent/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'; import { calculateCompressionThresholds } from '../../../../ai/llm/compress/constants'; +import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; +import { formatModelChars2Points } from '../../../../../support/wallet/usage/utils'; export const getMasterAgentSystemPrompt = async ({ steps, @@ -13,8 +15,8 @@ export const getMasterAgentSystemPrompt = async ({ background = '', model }: { - steps: AgentPlanStepType[]; - step: AgentPlanStepType; + steps: AgentStepItemType[]; + step: AgentStepItemType; userInput: string; background?: string; model: string; @@ -27,18 +29,21 @@ export const getMasterAgentSystemPrompt = async ({ stepPrompt: string, model: string, currentDescription: string - ): Promise => { - if (!stepPrompt) return stepPrompt; + ): Promise<{ + stepPrompt: string; + usage?: ChatNodeUsageType; + }> => { + if (!stepPrompt) return { stepPrompt }; const modelData = getLLMModel(model); - if (!modelData) return stepPrompt; + if (!modelData) return { stepPrompt }; const tokenCount = await countPromptTokens(stepPrompt); const thresholds = calculateCompressionThresholds(modelData.maxContext); const maxTokenThreshold = thresholds.dependsOn.threshold; if (tokenCount <= maxTokenThreshold) { - return stepPrompt; + return { stepPrompt: stepPrompt }; } const targetTokens = thresholds.dependsOn.target; @@ -129,7 +134,7 @@ ${stepPrompt} 请直接输出压缩后的步骤历史:`; try { - const { answerText } = await createLLMResponse({ + const { answerText, usage } = await createLLMResponse({ body: { model: modelData, messages: [ @@ -147,11 +152,26 @@ ${stepPrompt} } }); - return answerText || stepPrompt; + const { totalPoints, modelName } = formatModelChars2Points({ + model: modelData.model, + inputTokens: usage.inputTokens, + outputTokens: usage.outputTokens + }); + + return { + stepPrompt: answerText || stepPrompt, + usage: { + moduleName: '压缩步骤提示词', + model: modelName, + totalPoints, + inputTokens: usage.inputTokens, + outputTokens: usage.outputTokens + } + }; } catch (error) { console.error('压缩 stepPrompt 失败:', error); // 压缩失败时返回原始内容 - return stepPrompt; + return { stepPrompt: stepPrompt }; } }; @@ -163,11 +183,18 @@ ${stepPrompt} ) .filter(Boolean) .join('\n'); - addLog.debug(`Step call depends_on (LLM): ${step.id}`, step.depends_on); + addLog.debug(`Step call depends_on (LLM): ${step.id}, dependOn: ${step.depends_on}`); // 压缩依赖的上下文 - stepPrompt = await compressStepPrompt(stepPrompt, model, step.description || step.title); + const compressResult = await compressStepPrompt( + stepPrompt, + model, + step.description || step.title + ); + stepPrompt = compressResult.stepPrompt; - return `请根据任务背景、之前步骤的执行结果和当前步骤要求选择并调用相应的工具。如果是一个总结性质的步骤,请整合之前步骤的结果进行总结。 + return { + usage: compressResult.usage, + prompt: `请根据任务背景、之前步骤的执行结果和当前步骤要求选择并调用相应的工具。如果是一个总结性质的步骤,请整合之前步骤的结果进行总结。 【任务背景】 目标: ${userInput} 前置信息: ${background} @@ -192,5 +219,6 @@ ${stepPrompt} 6. 确保当前步骤的执行能够有效利用和整合前面的结果 7. 如果是总结的步骤,请利用之前步骤的信息进行全面总结 - 请严格按照步骤描述执行,确保完成所有要求的子任务。`; + 请严格按照步骤描述执行,确保完成所有要求的子任务。` + }; }; 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 b237675fe4..a5bfec11b7 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/master/call.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/master/call.ts @@ -9,7 +9,6 @@ import type { GetSubAppInfoFnType, SubAppRuntimeType } from '../type'; import { getMasterAgentSystemPrompt } from '../constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; -import { getWorkflowChildResponseWrite } from '../../../utils'; import { SubAppIds } from '../sub/constants'; import { parseJsonArgs } from '../../../../../ai/utils'; import { dispatchFileRead } from '../sub/file'; @@ -23,6 +22,7 @@ import type { DispatchPlanAgentResponse } from '../sub/plan'; import { dispatchPlanAgent } from '../sub/plan'; import { addLog } from '../../../../../../common/system/log'; import type { WorkflowResponseItemType } from '../../../type'; +import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; type Response = { stepResponse?: { @@ -32,6 +32,10 @@ type Response = { planResponse?: DispatchPlanAgentResponse; completeMessages: ChatCompletionMessageParam[]; assistantMessages: ChatCompletionMessageParam[]; + + inputTokens: number; + outputTokens: number; + subAppUsages: ChatNodeUsageType[]; }; export const masterCall = async ({ @@ -96,14 +100,16 @@ export const masterCall = async ({ step.depends_on = depends; } // Step call system prompt - // TODO: 需要把压缩的 usage 返回 - const systemPromptContent = await getMasterAgentSystemPrompt({ + const compressResult = await getMasterAgentSystemPrompt({ steps, step, userInput: userChatInput, model, background: systemPrompt }); + if (compressResult.usage) { + usagePush([compressResult.usage]); + } const requestMessages = chats2GPTMessages({ messages: [ @@ -112,7 +118,7 @@ export const masterCall = async ({ value: [ { text: { - content: systemPromptContent + content: compressResult.prompt } } ] @@ -160,244 +166,241 @@ export const masterCall = async ({ let planResult: DispatchPlanAgentResponse | undefined; - const { - assistantMessages, - completeMessages, - inputTokens, - outputTokens, - subAppUsages, - interactiveResponse - } = await runAgentCall({ - maxRunAgentTimes: 100, - body: { - messages: requestMessages, - model: getLLMModel(model), - temperature, - stream, - top_p: aiChatTopP, - tools: completionTools - }, + const { assistantMessages, completeMessages, inputTokens, outputTokens, subAppUsages } = + await runAgentCall({ + maxRunAgentTimes: 100, + body: { + messages: requestMessages, + model: getLLMModel(model), + temperature, + stream, + top_p: aiChatTopP, + tools: completionTools + }, - userKey: externalProvider.openaiAccount, - isAborted: checkIsStopping, - // childrenInteractiveParams + userKey: externalProvider.openaiAccount, + usagePush, + isAborted: checkIsStopping, + // childrenInteractiveParams - onReasoning({ text }) { - stepStreamResponse?.({ - event: SseResponseEventEnum.answer, - data: textAdaptGptResponse({ - reasoning_content: text - }) - }); - }, - onStreaming({ text }) { - stepStreamResponse?.({ - event: SseResponseEventEnum.answer, - data: textAdaptGptResponse({ - text - }) - }); - }, - onToolCall({ call }) { - const subApp = getSubAppInfo(call.function.name); + onReasoning({ text }) { + stepStreamResponse?.({ + event: SseResponseEventEnum.answer, + data: textAdaptGptResponse({ + reasoning_content: text + }) + }); + }, + onStreaming({ text }) { + stepStreamResponse?.({ + event: SseResponseEventEnum.answer, + data: textAdaptGptResponse({ + text + }) + }); + }, + onToolCall({ call }) { + const subApp = getSubAppInfo(call.function.name); - if (call.function.name === SubAppIds.plan) { - return; - } - - stepStreamResponse?.({ - id: call.id, - event: SseResponseEventEnum.toolCall, - data: { - tool: { - id: call.id, - toolName: subApp?.name || call.function.name, - toolAvatar: subApp?.avatar || '', - functionName: call.function.name, - params: call.function.arguments ?? '' - } + if (call.function.name === SubAppIds.plan) { + return; } - }); - }, - onToolParam({ tool, params }) { - stepStreamResponse?.({ - id: tool.id, - event: SseResponseEventEnum.toolParams, - data: { - tool: { - params + + stepStreamResponse?.({ + id: call.id, + event: SseResponseEventEnum.toolCall, + data: { + tool: { + id: call.id, + toolName: subApp?.name || call.function.name, + toolAvatar: subApp?.avatar || '', + functionName: call.function.name, + params: call.function.arguments ?? '' + } } - } - }); - }, - handleToolResponse: async ({ call, messages }) => { - addLog.debug('handleToolResponse', { toolName: call.function.name }); - const toolId = call.function.name; + }); + }, + onToolParam({ tool, params }) { + stepStreamResponse?.({ + id: tool.id, + event: SseResponseEventEnum.toolParams, + data: { + tool: { + params + } + } + }); + }, + handleToolResponse: async ({ call, messages }) => { + addLog.debug('handleToolResponse', { toolName: call.function.name }); + const toolId = call.function.name; - const { - response, - usages = [], - stop = false - } = await (async () => { - try { - if (toolId === SubAppIds.fileRead) { - const toolParams = ReadFileToolSchema.safeParse(parseJsonArgs(call.function.arguments)); + const { + response, + usages = [], + stop = false + } = await (async () => { + try { + if (toolId === SubAppIds.fileRead) { + const toolParams = ReadFileToolSchema.safeParse( + parseJsonArgs(call.function.arguments) + ); - if (!toolParams.success) { + if (!toolParams.success) { + return { + response: toolParams.error.message, + usages: [] + }; + } + const params = toolParams.data; + + const files = params.file_indexes.map((index) => ({ + index, + url: filesMap[index] + })); + const result = await dispatchFileRead({ + files, + teamId: runningUserInfo.teamId, + tmbId: runningUserInfo.tmbId, + customPdfParse: chatConfig?.fileSelectConfig?.customPdfParse + }); return { - response: toolParams.error.message, - usages: [] + response: result.response, + usages: result.usages }; } - const params = toolParams.data; + if (toolId === SubAppIds.plan) { + try { + planResult = await dispatchPlanAgent({ + checkIsStopping, + historyMessages: historiesMessages, + userInput: userChatInput, + completionTools, + getSubAppInfo, + systemPrompt: systemPrompt, + model, + stream + }); - const files = params.file_indexes.map((index) => ({ - index, - url: filesMap[index] - })); - const result = await dispatchFileRead({ - files, - teamId: runningUserInfo.teamId, - tmbId: runningUserInfo.tmbId, - customPdfParse: chatConfig?.fileSelectConfig?.customPdfParse - }); + return { + response: '', + stop: true + }; + } catch (error) { + console.log(error, 111); + return { + response: getErrText(error), + stop: false + }; + } + } + // User Sub App + else { + const tool = subAppsMap.get(toolId); + if (!tool) { + return { + response: 'Can not find the tool', + usages: [] + }; + } + + const toolCallParams = parseJsonArgs(call.function.arguments); + + if (!toolCallParams) { + return { + response: 'params is not object', + usages: [] + }; + } + + // Get params + const requestParams = { + ...tool.params, + ...toolCallParams + }; + + if (tool.type === 'tool') { + const { response, usages } = await dispatchTool({ + tool: { + name: tool.name, + version: tool.version, + toolConfig: tool.toolConfig + }, + params: requestParams, + runningUserInfo, + runningAppInfo, + variables, + workflowStreamResponse: stepStreamResponse + }); + return { + response, + usages + }; + } else if (tool.type === 'workflow' || tool.type === 'toolWorkflow') { + // const fn = tool.type === 'workflow' ? dispatchApp : dispatchPlugin; + + // const { response, usages } = await fn({ + // ...props, + // node, + // workflowStreamResponse:stepStreamResponse, + // callParams: { + // appId: node.pluginId, + // version: node.version, + // ...requestParams + // } + // }); + + // return { + // response, + // usages + // }; + return { + response: 'Can not find the tool', + usages: [] + }; + } else { + return { + response: 'Can not find the tool', + usages: [] + }; + } + } + } catch (error) { return { - response: result.response, - usages: result.usages + response: getErrText(error), + usages: [] }; } - if (toolId === SubAppIds.plan) { - try { - planResult = await dispatchPlanAgent({ - checkIsStopping, - historyMessages: historiesMessages, - userInput: userChatInput, - completionTools, - getSubAppInfo, - systemPrompt: systemPrompt, - model, - stream - }); + })(); - return { - response: '', - stop: true - }; - } catch (error) { - console.log(error, 111); - return { - response: getErrText(error), - stop: false - }; + // Push stream response + stepStreamResponse?.({ + id: call.id, + event: SseResponseEventEnum.toolResponse, + data: { + tool: { + response } } - // User Sub App - else { - const tool = subAppsMap.get(toolId); - if (!tool) { - return { - response: 'Can not find the tool', - usages: [] - }; - } + }); - const toolCallParams = parseJsonArgs(call.function.arguments); + // TODO: 推送账单 - if (!toolCallParams) { - return { - response: 'params is not object', - usages: [] - }; - } - - // Get params - const requestParams = { - ...tool.params, - ...toolCallParams - }; - - if (tool.type === 'tool') { - const { response, usages } = await dispatchTool({ - tool: { - name: tool.name, - version: tool.version, - toolConfig: tool.toolConfig - }, - params: requestParams, - runningUserInfo, - runningAppInfo, - variables, - workflowStreamResponse: stepStreamResponse - }); - return { - response, - usages - }; - } else if (tool.type === 'workflow' || tool.type === 'toolWorkflow') { - // const fn = tool.type === 'workflow' ? dispatchApp : dispatchPlugin; - - // const { response, usages } = await fn({ - // ...props, - // node, - // workflowStreamResponse:stepStreamResponse, - // callParams: { - // appId: node.pluginId, - // version: node.version, - // ...requestParams - // } - // }); - - // return { - // response, - // usages - // }; - return { - response: 'Can not find the tool', - usages: [] - }; - } else { - return { - response: 'Can not find the tool', - usages: [] - }; - } - } - } catch (error) { - return { - response: getErrText(error), - usages: [] - }; - } - })(); - - // Push stream response - stepStreamResponse?.({ - id: call.id, - event: SseResponseEventEnum.toolResponse, - data: { - tool: { - response - } - } - }); - - // TODO: 推送账单 - - return { - response, - assistantMessages: [], // TODO - usages, - stop - }; - }, - handleInteractiveTool: async ({ toolParams }) => { - return { - response: 'Interactive tool not supported', - assistantMessages: [], // TODO - usages: [] - }; - } - }); + return { + response, + assistantMessages: [], // TODO + usages, + stop + }; + }, + handleInteractiveTool: async ({ toolParams }) => { + return { + response: 'Interactive tool not supported', + assistantMessages: [], // TODO + usages: [] + }; + } + }); // Step call if (isStepCall) { @@ -431,7 +434,10 @@ export const masterCall = async ({ summary }, completeMessages, - assistantMessages + assistantMessages, + inputTokens, + outputTokens, + subAppUsages }; } @@ -439,6 +445,9 @@ export const masterCall = async ({ return { planResponse: planResult, completeMessages, - assistantMessages + assistantMessages, + inputTokens, + outputTokens, + subAppUsages }; }; diff --git a/packages/service/core/workflow/dispatch/ai/agent/master/dependon.ts b/packages/service/core/workflow/dispatch/ai/agent/master/dependon.ts index 7a025e9503..14e6765d78 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/master/dependon.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/master/dependon.ts @@ -1,5 +1,5 @@ import { getLLMModel } from '../../../../../ai/model'; -import type { AgentPlanStepType } from '../sub/plan/type'; +import type { AgentStepItemType } from '@fastgpt/global/core/ai/agent/type'; import { addLog } from '../../../../../../common/system/log'; import { createLLMResponse } from '../../../../../ai/llm/request'; import { parseJsonArgs } from '../../../../../ai/utils'; @@ -12,8 +12,8 @@ export const getStepDependon = async ({ step }: { model: string; - steps: AgentPlanStepType[]; - step: AgentPlanStepType; + steps: AgentStepItemType[]; + step: AgentStepItemType; }): Promise<{ depends: string[]; usage?: ChatNodeUsageType; 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 cf504eaf8a..bf0c08ddb4 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 @@ -251,6 +251,7 @@ export const dispatchReplanAgent = async ({ }: DispatchPlanAgentProps & { plan: AgentPlanType; }): Promise => { + const usages: ChatNodeUsageType[] = []; const modelData = getLLMModel(model); const requestMessages: ChatCompletionMessageParam[] = [ @@ -292,7 +293,10 @@ export const dispatchReplanAgent = async ({ description: '本步骤分析先前的执行结果,以确定重新规划时需要依赖哪些特定步骤。' } }); - // TODO: 推送 + + if (dependsUsage) { + usages.push(dependsUsage); + } const replanSteps = plan.steps.filter((step) => depends.includes(step.id)); requestMessages.push({ @@ -347,19 +351,18 @@ export const dispatchReplanAgent = async ({ inputTokens: usage.inputTokens, outputTokens: usage.outputTokens }); + usages.push({ + moduleName: '重新规划', + model: modelName, + totalPoints, + inputTokens: usage.inputTokens, + outputTokens: usage.outputTokens + }); return { askInteractive, plan: rePlan, completeMessages, - usages: [ - { - moduleName: '重新规划', - model: modelName, - totalPoints, - inputTokens: usage.inputTokens, - outputTokens: usage.outputTokens - } - ] + usages }; }; diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts index ec43ddc366..a1e81b7e12 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts @@ -125,7 +125,10 @@ export const dispatchTool = async ({ } const usagePoints = (() => { - if (params.system_input_config?.type !== SystemToolSecretInputTypeEnum.system) { + if ( + params.system_input_config?.type === SystemToolSecretInputTypeEnum.team || + params.system_input_config?.type === SystemToolSecretInputTypeEnum.manual + ) { return 0; } return (tool.systemKeyCost ?? 0) + (tool.currentCost ?? 0);