Commit 199c0dab by Archer Committed by GitHub

feat(agent): support helper tool loops and injected prompts (#7439)

* feat(agent): support helper tool loops and injected prompts

* prompt

* remove log
parent 34632ba4
# 辅助生成当前设计 # 辅助生成当前设计
状态:当前实现 状态:实现
最后核对:2026-07-16 最后核对:2026-08-03
## 适用范围 ## 适用范围
...@@ -48,16 +48,41 @@ API route ...@@ -48,16 +48,41 @@ API route
## Agent Loop 接入 ## Agent Loop 接入
`runAuxiliaryGenerationAgentLoop` 复用 [Agent Loop](./agent-loop/index.md)当前约束如下: `runAuxiliaryGenerationAgentLoop` 复用 [Agent Loop](./agent-loop/index.md),约束如下:
- 启用 `plan` 系统工具。 - 不启用 `plan`、Sandbox、文件读取或知识库系统工具。
- runtime tool catalog 为空 - 启用标准 `ask_user` 系统工具;暂停和恢复完全遵循 Agent Loop 的 `providerState + userAnswer` 协议
- 不启用 ask、Sandbox、文件读取或知识库工具 - 业务调用方可以显式注入 runtime tools 和 executor;Chat Agent Helper 注入 `generate_config`
- reasoning delta 转为辅助生成 answer SSE。 - reasoning delta 转为辅助生成 answer SSE。
- usage 直接进入辅助生成 usage sink。 - usage 直接进入辅助生成 usage sink。
- 最终只提取不含 tool calls 的 assistant message,返回 answer 和 reasoning 文本 - 结果保留标准 `status``pause``providerState`,业务层只负责转换展示和持久化,不自行判断暂停条件
如果新场景需要业务工具或 interactive,应先设计显式能力协议,不能依赖 processor 闭包隐式访问 Workflow runtime。 如果新场景需要业务工具,必须通过 runtime tool catalog 和 executor 显式注入,不能依赖 processor 读取 Workflow runtime。
## Chat Agent Helper 连续调用
```text
模型调用 ask_user
-> Agent Loop 返回 paused + ask + providerState
-> Chat Agent Helper 保存 interactive、ask tool call 和 providerState memory
-> 用户提交与 Workflow Agent 相同的 { answers: string[] } 原始结构
-> 调用方传回 providerState + userAnswer
-> Agent Loop 在原 ask tool call 后追加 tool response 并继续
-> 模型调用 generate_config
-> executor 校验并生成表单配置,返回 "Generate config success"
-> 模型自行结束,调用方清理 providerState memory
```
Chat Agent Helper 读取历史时使用 `reserveTool: true`。除 interactive 外,还需要持久化对应的 `ask_user``generate_config` tool call/response,否则历史转换无法恢复工具语义。
`generate_config` 是普通 runtime tool,不设置 `stop`。工具参数使用配置生成业务结构,不包含用于旧 JSON 路由的 `phase``reasoning` 字段;executor 使用 Zod 校验,并确认全部资源 ID 都在当前成员的可访问资源集合内,再转换为最终表单结构。参数错误作为 tool error 返回给模型修正,不再额外调用模型修复 JSON。
## Provider State 持久化
- 暂停态把 Agent Loop 返回的完整 `providerState` 写入当前 AI ChatItem 的 `memories`
- 恢复态只从最后一条 AI history 读取该 memory,并把原始回答作为 `userAnswer` 传入。
- `done``error``aborted` 都清除该 memory,避免后续普通消息恢复陈旧暂停点。
- 通用 `saveChat` 已支持 memories;辅助生成只扩展 processor 返回协议和 Chat Agent Helper 保存调用,不修改通用保存语义。
## SSE 与断流续传 ## SSE 与断流续传
......
...@@ -57,7 +57,7 @@ export const generateSandboxId = ({ ...@@ -57,7 +57,7 @@ export const generateSandboxId = ({
// Prompt // Prompt
export const SANDBOX_USER_FILES_PATH = 'user_files/'; export const SANDBOX_USER_FILES_PATH = 'user_files/';
export const SANDBOX_ENTRYPOINT_MAX_LENGTH = 16 * 1024; export const SANDBOX_ENTRYPOINT_MAX_LENGTH = 16 * 1024;
export const SANDBOX_SYSTEM_PROMPT = `## 沙盒能力 export const SANDBOX_SYSTEM_PROMPT = `<sandbox_capability>
你拥有一个独立的 Linux 沙盒环境(Ubuntu 22.04),可通过 sandbox 工具操作文件和执行命令。 你拥有一个独立的 Linux 沙盒环境(Ubuntu 22.04),可通过 sandbox 工具操作文件和执行命令。
- 系统预装:bash / python3 / node / bun / git / curl - 系统预装:bash / python3 / node / bun / git / curl
- 用户对话上传的文件存储在 ${SANDBOX_USER_FILES_PATH} 目录下 - 用户对话上传的文件存储在 ${SANDBOX_USER_FILES_PATH} 目录下
...@@ -70,4 +70,5 @@ export const SANDBOX_SYSTEM_PROMPT = `## 沙盒能力 ...@@ -70,4 +70,5 @@ export const SANDBOX_SYSTEM_PROMPT = `## 沙盒能力
- 使用 ${SANDBOX_LS_TOOL_NAME} 列出目录内容,优先于通过 shell 调用 ls - 使用 ${SANDBOX_LS_TOOL_NAME} 列出目录内容,优先于通过 shell 调用 ls
- 默认将生成文件保存在当前 sandbox 工作目录;若本轮 system-reminder 指定了更具体的产物目录或禁止目录,必须优先遵守 - 默认将生成文件保存在当前 sandbox 工作目录;若本轮 system-reminder 指定了更具体的产物目录或禁止目录,必须优先遵守
- HTML 等多文件预览产物必须使用相对资源路径(例如 ./assets/app.js),不要使用 /assets/app.js 这类根路径 - HTML 等多文件预览产物必须使用相对资源路径(例如 ./assets/app.js),不要使用 /assets/app.js 这类根路径
- 若需要将生成的文件链接,可使用 ${SANDBOX_GET_FILE_URL_TOOL_NAME} 获取临时访问链接`; - 若需要将生成的文件链接,可使用 ${SANDBOX_GET_FILE_URL_TOOL_NAME} 获取临时访问链接
</sandbox_capability>`;
import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/llm/type'; import type {
ChatCompletionMessageParam,
ChatCompletionTool
} from '@fastgpt/global/core/ai/llm/type';
import { runAgentLoop } from '../llm/agentLoop/interface'; import { runAgentLoop } from '../llm/agentLoop/interface';
import type { AgentLoopRuntime } from '../llm/agentLoop/interface';
import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
import type { AuxiliaryGenerationStreamWriter } from './stream'; import type { AuxiliaryGenerationStreamWriter } from './stream';
import { AuxiliaryGenerationEventEnum } from '@fastgpt/global/core/ai/auxiliaryGeneration/constants'; import { AuxiliaryGenerationEventEnum } from '@fastgpt/global/core/ai/auxiliaryGeneration/constants';
...@@ -16,13 +20,18 @@ type RunAuxiliaryGenerationAgentLoopParams = { ...@@ -16,13 +20,18 @@ type RunAuxiliaryGenerationAgentLoopParams = {
streamWriter?: AuxiliaryGenerationStreamWriter; streamWriter?: AuxiliaryGenerationStreamWriter;
checkIsStopping?: () => boolean; checkIsStopping?: () => boolean;
usageSink?: (usages: ChatNodeUsageType[]) => void; usageSink?: (usages: ChatNodeUsageType[]) => void;
providerState?: unknown;
userAnswer?: string;
runtimeTools?: ChatCompletionTool[];
executeTool?: AgentLoopRuntime['executeTool'];
}; };
/** /**
* 运行辅助生成的无工具 Agent Loop。 * 运行辅助生成 Agent Loop。
* *
* 该入口只封装统一 agent loop 的模型循环和计划维护,不注册 runtime tools, * systemPrompt 作为调用方提供的最终提示词原样传入。该入口启用标准 ask_user,并允许
* 因此不会隐式获得 workflow、Skill 或虚拟机执行能力。 * 业务方显式注入 runtime tools;不会隐式获得默认 Agent 提示词、workflow、Skill、计划
* 或虚拟机执行能力。paused/providerState 等结果保持 Agent Loop 原语义。
*/ */
export async function runAuxiliaryGenerationAgentLoop({ export async function runAuxiliaryGenerationAgentLoop({
teamId, teamId,
...@@ -34,7 +43,11 @@ export async function runAuxiliaryGenerationAgentLoop({ ...@@ -34,7 +43,11 @@ export async function runAuxiliaryGenerationAgentLoop({
useVideo, useVideo,
streamWriter, streamWriter,
checkIsStopping, checkIsStopping,
usageSink usageSink,
providerState,
userAnswer,
runtimeTools = [],
executeTool
}: RunAuxiliaryGenerationAgentLoopParams) { }: RunAuxiliaryGenerationAgentLoopParams) {
const result = await runAgentLoop({ const result = await runAgentLoop({
runtime: { runtime: {
...@@ -47,14 +60,16 @@ export async function runAuxiliaryGenerationAgentLoop({ ...@@ -47,14 +60,16 @@ export async function runAuxiliaryGenerationAgentLoop({
useVideo useVideo
}, },
systemTools: { systemTools: {
plan: { enabled: true } ask: { enabled: true }
}, },
toolCatalog: { toolCatalog: {
runtimeTools: [] runtimeTools
},
executeTool: async () => {
throw new Error('Auxiliary generation does not support runtime tools');
}, },
executeTool:
executeTool ??
(async () => {
throw new Error('Auxiliary generation runtime tool executor is not configured');
}),
checkIsStopping, checkIsStopping,
emitEvent: (event) => { emitEvent: (event) => {
if (event.type === 'reasoning_delta') { if (event.type === 'reasoning_delta') {
...@@ -63,12 +78,20 @@ export async function runAuxiliaryGenerationAgentLoop({ ...@@ -63,12 +78,20 @@ export async function runAuxiliaryGenerationAgentLoop({
data: createChatCompletionDeltaResponse({ reasoningContent: event.text }) data: createChatCompletionDeltaResponse({ reasoningContent: event.text })
}); });
} }
if (event.type === 'answer_delta') {
streamWriter?.({
event: AuxiliaryGenerationEventEnum.answer,
data: createChatCompletionDeltaResponse({ text: event.text })
});
}
}, },
usagePush: usageSink usagePush: usageSink
}, },
input: { input: {
systemPrompt, systemPrompt,
messages messages,
providerState,
userAnswer
} }
}); });
...@@ -86,7 +109,7 @@ export async function runAuxiliaryGenerationAgentLoop({ ...@@ -86,7 +109,7 @@ export async function runAuxiliaryGenerationAgentLoop({
.join(''); .join('');
return { return {
status: result.status, ...result,
answerText, answerText,
reasoningText reasoningText
}; };
......
...@@ -22,6 +22,7 @@ export const runAuxiliaryGeneration = async <T>({ ...@@ -22,6 +22,7 @@ export const runAuxiliaryGeneration = async <T>({
sourceId, sourceId,
chatId, chatId,
query, query,
userAnswer,
files, files,
data, data,
histories, histories,
...@@ -69,6 +70,7 @@ export const runAuxiliaryGeneration = async <T>({ ...@@ -69,6 +70,7 @@ export const runAuxiliaryGeneration = async <T>({
try { try {
const result = await processor({ const result = await processor({
query, query,
userAnswer,
files, files,
data, data,
histories, histories,
......
...@@ -17,6 +17,7 @@ export type AuxiliaryGenerationUser = { ...@@ -17,6 +17,7 @@ export type AuxiliaryGenerationUser = {
export type AuxiliaryGenerationProcessorParams<T = unknown> = { export type AuxiliaryGenerationProcessorParams<T = unknown> = {
query: string; query: string;
userAnswer?: string;
files: AuxiliaryGenerationChatFileType[]; files: AuxiliaryGenerationChatFileType[];
data: T; data: T;
histories: ChatItemDBSchemaType[]; histories: ChatItemDBSchemaType[];
...@@ -31,6 +32,7 @@ export type AuxiliaryGenerationProcessorParams<T = unknown> = { ...@@ -31,6 +32,7 @@ export type AuxiliaryGenerationProcessorParams<T = unknown> = {
export type AuxiliaryGenerationProcessorResponse = { export type AuxiliaryGenerationProcessorResponse = {
aiResponse: AIChatItemValueItemType[]; aiResponse: AIChatItemValueItemType[];
memories?: Record<string, any>;
usage: { usage: {
model: string; model: string;
inputTokens: number; inputTokens: number;
...@@ -51,6 +53,7 @@ export type AuxiliaryGenerationRunParams<T = unknown> = { ...@@ -51,6 +53,7 @@ export type AuxiliaryGenerationRunParams<T = unknown> = {
sourceId: string; sourceId: string;
chatId: string; chatId: string;
query: string; query: string;
userAnswer?: string;
files: AuxiliaryGenerationChatFileType[]; files: AuxiliaryGenerationChatFileType[];
data: T; data: T;
histories: ChatItemDBSchemaType[]; histories: ChatItemDBSchemaType[];
......
...@@ -2,6 +2,7 @@ export * from './event'; ...@@ -2,6 +2,7 @@ export * from './event';
export * from './continuation'; export * from './continuation';
export * from './input'; export * from './input';
export * from './interactive'; export * from './interactive';
export * from './mainPrompt';
export * from './provider'; export * from './provider';
export * from './result'; export * from './result';
export * from './runtime'; export * from './runtime';
......
import { askUserToolName } from './systemTool/ask'; import { SANDBOX_SYSTEM_PROMPT } from '@fastgpt/global/core/ai/sandbox/constants';
import { setPlanToolName, updatePlanToolName } from './systemTool/plan';
/** const DEFAULT_AGENT_SYSTEM_PROMPT = `你是一个 Work Agent。
* 构建各 agent-loop provider 共用的主 Agent system prompt。
* workflow 侧可把用户配置、sandbox、知识库引用规则等合并到 systemPrompt 中传入。
*/
export const getMainAgentSystemPrompt = ({
systemPrompt,
hasRuntimeTools
}: {
systemPrompt?: string;
hasRuntimeTools: boolean;
}) => {
const askToolName = askUserToolName;
const createPlanToolName = setPlanToolName;
const maintainPlanToolName = updatePlanToolName;
return `<role>
你是 Work Agent。
你在一个工具循环中工作:阅读用户目标,调用工具获取信息或执行动作,维护计划状态,并在任务完成后给出最终回答。
</role>
${
systemPrompt
? `<user_background>
${systemPrompt}
</user_background>`
: ''
}
<operating_rules>
1. 如果当前问题可以直接回答,直接回答。
2. 如果任务复杂、包含多步骤、需要调研/比较/方案设计/连续工具调用,先调用 ${createPlanToolName} 创建 active plan;在计划创建成功前,不要先调用 runtime tool 探查上下文。
3. 已有 active plan,或任务不需要 plan 时,再按需调用合适的 runtime tool 获取外部信息或执行动作。
4. 如果已有 active plan,围绕它推进任务,并在步骤开始、完成、阻塞或需要新增步骤时调用 ${maintainPlanToolName}
5. 如果任务或 Skill 需要用户通过选项补充信息或做出有意义的选择,调用 ${askToolName};低影响细节可以合理假设。
6. 最终回答前可根据实际进度更新 active plan。
</operating_rules>
<tool_rules>
- runtime tools 用于真实业务动作,例如知识库检索、文件处理、sandbox、插件或用户选择工具。
- ${askToolName} 用于通过选项向用户收集信息或选择,包括 Skill 明确要求的用户确认。
- ${createPlanToolName} 只用于创建或替换 active plan;${maintainPlanToolName} 用于更新状态或追加步骤。
- 不要把 ${askToolName}${createPlanToolName}${maintainPlanToolName} 当成普通业务工具解释给用户。
- 工具返回结果后,根据结果继续执行、更新计划或最终回答。
</tool_rules>
${
!hasRuntimeTools
? `<tool_constraint>
当前没有可用的 runtime tools。
不要调用不存在的 runtime tool;如果可以直接回答就直接回答。复杂任务仍可用 ${createPlanToolName}${maintainPlanToolName} 维护计划,需要用户选择或补充信息时可用 ${askToolName}
</tool_constraint>`
: ''
}
<planning_rules>
默认不要过度规划。
需要 plan 的情况:多步骤探索、多个工具连续调用、比较/调研/方案设计、目标路径不确定、用户明确要求计划或拆解。
不需要 plan 的情况:闲聊、简单问答、单次工具调用即可完成、已有上下文足够总结回答。
硬性要求:如果用户明确要求“计划模式”、创建计划、拆解步骤、逐步执行或每步更新计划状态,${createPlanToolName} 必须是第一个工具调用。在它成功前,不要先调用 sandbox、检索或其他 runtime tool 探查上下文,也不能直接给最终回答。
</planning_rules>
<ask_rules>
以下情况可以调用 ${askToolName}
1. 任务或 Skill 明确需要通过选项向用户收集信息、确认需求或选择下一步。
2. 用户的偏好、范围、格式或执行路径会显著影响产物,直接假设可能造成明显返工。
3. 必须由用户提供私有文件、账号、凭据或业务数据。
4. 用户要求的工具不可用,需要用户选择替代策略。
5. 用户目标不明确,需要确认产物类型或成功标准。
调用 ${askToolName} 时必须提供: 根据用户目标和当前上下文完成任务。
- questions:1 到 3 个面向用户的简短问题。 需要执行动作时,调用当前提供的工具;不要调用不存在的工具。
- 每个问题提供 2 到 4 个候选答案(如无必要则给 3 个)。每个选项必须提供 summary 和 value:summary 是展示给用户的简短选项文本,value 是用户选中后返回给你的完整答案。 工具返回结果后,根据结果继续执行或给出最终回答。
`;
Skill 要求向用户收集选项信息时,优先遵循 Skill 并调用 ${askToolName},不要自行替用户选择。 export type BuildDefaultAgentSystemPromptParams = {
不要为了不会影响结果的琐碎细节、可以直接通过工具获得的信息,或只是让计划更完美而追问。 systemPromptExtension?: string;
</ask_rules> userSystemPrompt?: string;
sandboxEnabled?: boolean;
<plan_rules>
只有复杂任务才需要维护 active plan;基础任务、简单问答、闲聊、单次工具调用即可完成的任务,不要创建 plan。
${createPlanToolName}${maintainPlanToolName} 只用于维护当前任务的执行计划,不是最终回答。
工具参数:
- 创建计划:调用 ${createPlanToolName},参数格式为 {"name":"简短计划名","steps":["步骤一","步骤二"]}。steps 的每一项必须是字符串。
- 更新状态:调用 ${maintainPlanToolName},参数格式为 {"updates":[{"id":"已有步骤 id","status":"done","note":"简短结果"}]}。
- 追加步骤:调用 ${maintainPlanToolName},参数格式为 {"add_steps":["新增步骤"]}。updates 和 add_steps 可以在一次调用中同时提供。
- status 只能是 pending、in_progress、done、blocked、skipped。
- 不要传 action 或 description;不要把 set_plan.steps 或 update_plan.add_steps 写成对象数组;不要把 updates 写成 steps。
工作方式:
- 没有 active plan 且任务确实复杂时,调用 ${createPlanToolName},用必要的初始步骤创建计划。
- 已有 active plan 时不要再次调用 ${createPlanToolName};继续任务、改变步骤状态或扩展范围都使用 ${maintainPlanToolName}
- 任务推进过程中,如果发现需要新增工作,通过 ${maintainPlanToolName}.add_steps 追加步骤。
- 更新已有步骤时通过 ${maintainPlanToolName}.updates 只提交步骤 id、状态和可选备注。
- 当步骤开始、完成、受阻或不再需要时,及时更新对应步骤状态。
- 步骤完成、受阻或跳过时,在备注中写清楚简短结果或原因。
- 不要删除步骤;不需要的步骤标记为跳过。
- 最终回答前,确保已有 plan 已经完成、跳过或明确阻塞。
</plan_rules>
<completion_rules>
任务已经有足够结果,或当前阶段适合先向用户反馈时,直接回答。
active plan 只是 Todo 和进度记录;存在 pending 或 in_progress step 不阻止最终回答。
如果任务中途结束,通过 ${maintainPlanToolName} 记录当前进度或阻塞原因。
</completion_rules>
<output_guidelines>
- 直接给用户有用结果,不解释内部路由。
- 最终回答要总结完成内容、关键依据、阻塞项或下一步建议。
</output_guidelines>`;
}; };
/**
* 构建 Agent 的最终 system prompt。
*
* AgentLoop provider 只消费该方法返回的最终文本;沙盒能力和平台扩展作为默认区块注入,
* userSystemPrompt 区块只保留用户配置。
*/
export const buildDefaultAgentSystemPrompt = ({
systemPromptExtension,
userSystemPrompt,
sandboxEnabled = false
}: BuildDefaultAgentSystemPromptParams = {}) =>
[
DEFAULT_AGENT_SYSTEM_PROMPT.trim(),
sandboxEnabled ? SANDBOX_SYSTEM_PROMPT.trim() : undefined,
systemPromptExtension?.trim() || undefined,
userSystemPrompt?.trim()
? `<user_system_prompt>\n${userSystemPrompt.trim()}\n</user_system_prompt>`
: undefined
]
.filter(Boolean)
.join('\n\n');
...@@ -14,7 +14,6 @@ import type { AgentLoopUsage } from './usage'; ...@@ -14,7 +14,6 @@ import type { AgentLoopUsage } from './usage';
export type AgentLoopLLMParams = { export type AgentLoopLLMParams = {
model: string; model: string;
promptMode?: 'fastAgent' | 'raw';
reasoningEffort?: CreateLLMResponseProps['body']['reasoning_effort']; reasoningEffort?: CreateLLMResponseProps['body']['reasoning_effort'];
userKey?: OpenaiAccountType; userKey?: OpenaiAccountType;
stream?: boolean; stream?: boolean;
......
...@@ -66,7 +66,6 @@ export const runFastAgentLoop = async <TChildrenResponse = unknown>({ ...@@ -66,7 +66,6 @@ export const runFastAgentLoop = async <TChildrenResponse = unknown>({
const fastAgentRuntime: FastAgentInternalRuntime<TChildrenResponse> = { const fastAgentRuntime: FastAgentInternalRuntime<TChildrenResponse> = {
teamId: runtime.teamId, teamId: runtime.teamId,
model: runtime.llmParams.model, model: runtime.llmParams.model,
promptMode: runtime.llmParams.promptMode,
reasoningEffort: runtime.llmParams.reasoningEffort, reasoningEffort: runtime.llmParams.reasoningEffort,
userKey: runtime.llmParams.userKey, userKey: runtime.llmParams.userKey,
stream: runtime.llmParams.stream, stream: runtime.llmParams.stream,
......
...@@ -7,7 +7,6 @@ import type { AgentPlanType } from '@fastgpt/global/core/ai/agent/type'; ...@@ -7,7 +7,6 @@ import type { AgentPlanType } from '@fastgpt/global/core/ai/agent/type';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
import { parseJsonArgs } from '../../../../../utils'; import { parseJsonArgs } from '../../../../../utils';
import { runAgentLoop } from './base'; import { runAgentLoop } from './base';
import { getMainAgentSystemPrompt } from '../../../domain/mainPrompt';
import { import {
formatAgentAskToolResponse, formatAgentAskToolResponse,
parseAgentAskToolCall, parseAgentAskToolCall,
...@@ -100,31 +99,12 @@ const stripSystemMessages = (messages: ChatCompletionMessageParam[]) => ...@@ -100,31 +99,12 @@ const stripSystemMessages = (messages: ChatCompletionMessageParam[]) =>
/** /**
* 构建进入主 Agent 的初始消息链。 * 构建进入主 Agent 的初始消息链。
* raw 模式完全尊重调用方传入的 messages;fastAgent 模式会注入平台主提示词并剔除外部 system * systemPrompt 是调用方已经组装完成的最终提示词;messages 中的 system message 不再重复注入
*/ */
const buildInitialMessages = ({ const buildInitialMessages = ({ input }: { input: FastAgentLoopInput }) => [
input, ...(input.systemPrompt ? [createSystemMessage(input.systemPrompt)] : []),
hasRuntimeTools,
promptMode = 'fastAgent'
}: {
input: FastAgentLoopInput;
hasRuntimeTools: boolean;
promptMode?: AgentLoopRuntime['promptMode'];
}): ChatCompletionMessageParam[] => {
if (promptMode === 'raw') {
return input.messages;
}
return [
createSystemMessage(
getMainAgentSystemPrompt({
systemPrompt: input.systemPrompt,
hasRuntimeTools
})
),
...stripSystemMessages(input.messages) ...stripSystemMessages(input.messages)
]; ];
};
/** /**
* ask_user 暂停时保存恢复所需上下文。 * ask_user 暂停时保存恢复所需上下文。
...@@ -186,12 +166,6 @@ export const runFastAgentMainLoop = async <TChildrenResponse = unknown>({ ...@@ -186,12 +166,6 @@ export const runFastAgentMainLoop = async <TChildrenResponse = unknown>({
toolCatalog: normalized toolCatalog: normalized
}; };
const hasRuntimeTools =
normalized.runtimeTools.length > 0 ||
(normalized.sandboxTools?.length ?? 0) > 0 ||
!!normalized.readFileTool ||
!!normalized.datasetSearchTool;
let pendingAsk: let pendingAsk:
| { | {
ask: AgentAskPayload; ask: AgentAskPayload;
...@@ -218,7 +192,7 @@ export const runFastAgentMainLoop = async <TChildrenResponse = unknown>({ ...@@ -218,7 +192,7 @@ export const runFastAgentMainLoop = async <TChildrenResponse = unknown>({
) )
} as ChatCompletionMessageParam } as ChatCompletionMessageParam
] ]
: buildInitialMessages({ input, hasRuntimeTools, promptMode: runtime.promptMode }); : buildInitialMessages({ input });
// 普通续轮通过 input.activePlan 恢复结构化 plan;ask_user 续跑则优先使用暂停时的完整快照。 // 普通续轮通过 input.activePlan 恢复结构化 plan;ask_user 续跑则优先使用暂停时的完整快照。
// 历史 checkpoint 只负责给模型提供上下文,不再作为运行时状态的反序列化来源。 // 历史 checkpoint 只负责给模型提供上下文,不再作为运行时状态的反序列化来源。
let activePlan = input.pendingMainContext?.activePlan ?? input.activePlan; let activePlan = input.pendingMainContext?.activePlan ?? input.activePlan;
......
...@@ -32,7 +32,6 @@ export type { ...@@ -32,7 +32,6 @@ export type {
export type AgentLoopRuntime<TChildrenResponse = unknown> = { export type AgentLoopRuntime<TChildrenResponse = unknown> = {
teamId: string; teamId: string;
model: string; model: string;
promptMode?: 'fastAgent' | 'raw';
reasoningEffort?: CreateLLMResponseProps['body']['reasoning_effort']; reasoningEffort?: CreateLLMResponseProps['body']['reasoning_effort'];
userKey?: CreateLLMResponseProps['userKey']; userKey?: CreateLLMResponseProps['userKey'];
stream?: boolean; stream?: boolean;
......
...@@ -15,7 +15,6 @@ import { loadRequestMessages } from '../../../utils'; ...@@ -15,7 +15,6 @@ import { loadRequestMessages } from '../../../utils';
import { formatModelChars2Points } from '../../../../../../support/wallet/usage/utils'; import { formatModelChars2Points } from '../../../../../../support/wallet/usage/utils';
import { getLLMModel } from '../../../../model'; import { getLLMModel } from '../../../../model';
import { AgentUsageModuleName } from '../../domain/usage'; import { AgentUsageModuleName } from '../../domain/usage';
import { getMainAgentSystemPrompt } from '../../domain/mainPrompt';
import { import {
askUserToolName, askUserToolName,
formatAgentAskToolResponse, formatAgentAskToolResponse,
...@@ -408,20 +407,9 @@ export const runPiAgentLoop = async <TChildrenResponse = unknown>({ ...@@ -408,20 +407,9 @@ export const runPiAgentLoop = async <TChildrenResponse = unknown>({
const pendingRequests: Array<{ requestId: string; requestIndex: number; startedAt: number }> = []; const pendingRequests: Array<{ requestId: string; requestIndex: number; startedAt: number }> = [];
const maxRunAgentTimes = Math.max(1, runtime.maxRunAgentTimes ?? 100); const maxRunAgentTimes = Math.max(1, runtime.maxRunAgentTimes ?? 100);
const systemPrompt =
runtime.llmParams.promptMode === 'raw'
? input.systemPrompt || ''
: getMainAgentSystemPrompt({
systemPrompt: input.systemPrompt,
hasRuntimeTools:
runtime.toolCatalog.runtimeTools.length > 0 ||
runtime.systemTools?.sandbox?.enabled === true ||
runtime.systemTools?.readFile?.enabled === true ||
runtime.systemTools?.datasetSearch?.enabled === true
});
const agent = new Agent({ const agent = new Agent({
initialState: { initialState: {
systemPrompt, systemPrompt: input.systemPrompt ?? '',
model: piModel, model: piModel,
thinkingLevel: getPiThinkingLevel(modelName, runtime.llmParams.reasoningEffort), thinkingLevel: getPiThinkingLevel(modelName, runtime.llmParams.reasoningEffort),
tools, tools,
......
...@@ -7,7 +7,6 @@ import type { ...@@ -7,7 +7,6 @@ import type {
ChatItemMiniType ChatItemMiniType
} from '@fastgpt/global/core/chat/type'; } from '@fastgpt/global/core/chat/type';
import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants'; import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
import { SANDBOX_SYSTEM_PROMPT } from '@fastgpt/global/core/ai/sandbox/constants';
import type { AgentToolType } from '@fastgpt/global/core/app/tool/type'; import type { AgentToolType } from '@fastgpt/global/core/app/tool/type';
import type { ReasoningEffort } from '@fastgpt/global/core/ai/llm/type'; import type { ReasoningEffort } from '@fastgpt/global/core/ai/llm/type';
import type { SelectedAgentSkillItemType } from '@fastgpt/global/core/app/formEdit/type'; import type { SelectedAgentSkillItemType } from '@fastgpt/global/core/app/formEdit/type';
...@@ -36,13 +35,13 @@ import { ...@@ -36,13 +35,13 @@ import {
buildAgentLoopCoreFinalAssistantOutput, buildAgentLoopCoreFinalAssistantOutput,
buildAgentLoopCoreProviderStateMemories, buildAgentLoopCoreProviderStateMemories,
buildAgentLoopCoreRequestMessages, buildAgentLoopCoreRequestMessages,
buildAgentLoopCoreSystemPrompt,
createAgentLoopCoreChildInteractiveParams, createAgentLoopCoreChildInteractiveParams,
prepareAgentLoopCoreProviderRunState, prepareAgentLoopCoreProviderRunState,
readAgentLoopCoreActivePlan, readAgentLoopCoreActivePlan,
readAgentLoopCoreProviderStateMemory, readAgentLoopCoreProviderStateMemory,
runAgentLoopCoreWithSummary runAgentLoopCoreWithSummary
} from '../agentLoopCore/interface'; } from '../agentLoopCore/interface';
import { buildDefaultAgentSystemPrompt } from '../../../../ai/llm/agentLoop/interface';
export type DispatchAgentModuleProps = ModuleDispatchProps<{ export type DispatchAgentModuleProps = ModuleDispatchProps<{
[NodeInputKeyEnum.history]?: ChatItemMiniType[]; [NodeInputKeyEnum.history]?: ChatItemMiniType[];
...@@ -241,15 +240,11 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise ...@@ -241,15 +240,11 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
subAppsMap: agentSubAppsMap, subAppsMap: agentSubAppsMap,
lang lang
}); });
// system message 由 getMainAgentSystemPrompt 统一注入;历史里的 system 只作为外部噪音过滤掉 // 历史里的 system 只作为外部噪音过滤;最终 systemPrompt 在创建 runtime 后按工具能力构建
// PromptEditor 保存的是工具 ID,进入主 Agent 前转换为具体名称,避免模型看到不可调用的 ID。 // PromptEditor 保存的是工具 ID,进入主 Agent 前转换为具体名称,避免模型看到不可调用的 ID。
const formatedSystemPrompt = buildAgentLoopCoreSystemPrompt({ const formattedUserSystemPrompt = replaceAgentPromptToolReferences({
userSystemPrompt: replaceAgentPromptToolReferences({
text: systemPrompt, text: systemPrompt,
resolveName: (id) => resolveName: (id) => promptToolReferenceInfoMap.get(id) || getSubAppInfo(id).name || undefined
promptToolReferenceInfoMap.get(id) || getSubAppInfo(id).name || undefined
}),
runtimePrompts: sandboxClient ? [SANDBOX_SYSTEM_PROMPT] : []
}); });
// 2. 创建 workflow adapter。 // 2. 创建 workflow adapter。
...@@ -257,7 +252,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise ...@@ -257,7 +252,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
const { runtime, artifacts } = createWorkflowAgentLoopRuntime({ const { runtime, artifacts } = createWorkflowAgentLoopRuntime({
context: { context: {
...props, ...props,
systemPrompt: formatedSystemPrompt, systemPrompt: formattedUserSystemPrompt,
getSubAppInfo, getSubAppInfo,
getSubApp, getSubApp,
completionTools: agentCompletionTools, completionTools: agentCompletionTools,
...@@ -271,6 +266,10 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise ...@@ -271,6 +266,10 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
nodeResponses: childNodeResponses, nodeResponses: childNodeResponses,
appendNodeResponse: nodeResponseCollector.appendNodeResponse appendNodeResponse: nodeResponseCollector.appendNodeResponse
}); });
const agentSystemPrompt = buildDefaultAgentSystemPrompt({
userSystemPrompt: formattedUserSystemPrompt,
sandboxEnabled: !!sandboxClient
});
// providerState 统一保存 provider 内部恢复信息。 // providerState 统一保存 provider 内部恢复信息。
// fastAgent/piAgent 的 ask_user 都在其中保存标准 pendingMainContext,用户回答后恢复同一条 messages。 // fastAgent/piAgent 的 ask_user 都在其中保存标准 pendingMainContext,用户回答后恢复同一条 messages。
...@@ -292,7 +291,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise ...@@ -292,7 +291,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
runtime, runtime,
input: buildAgentLoopCoreInput({ input: buildAgentLoopCoreInput({
messages: loopMessages, messages: loopMessages,
systemPrompt: formatedSystemPrompt, systemPrompt: agentSystemPrompt,
activePlan, activePlan,
providerState: runtimeProviderState, providerState: runtimeProviderState,
userAnswer: isAskResume ? queryInput || userChatInput : undefined, userAnswer: isAskResume ? queryInput || userChatInput : undefined,
......
export * from './input'; export * from './input';
export * from './files'; export * from './files';
export * from './messages'; export * from './messages';
export * from './prompt';
export * from './reminder'; export * from './reminder';
/**
* 改写用户配置的主 Agent 背景 prompt。
*
* 这里只处理纯 prompt 文本,不读取 workflow、sandbox、skill 等业务状态;
* 调用方负责先把需要注入的运行上下文拼成 userSystemPrompt。
*/
export const parseAgentLoopCoreUserSystemPrompt = ({
userSystemPrompt
}: {
userSystemPrompt?: string;
}) => {
if (!userSystemPrompt) {
return '';
}
return `${userSystemPrompt}
请参考用户的任务信息来匹配是否和当前的 <user_background></user_background> 一致,如果一致请优先遵循参考的步骤安排和偏好
如果和 <user_background></user_background> 没有任何关系则忽略参考信息。
**重要**:如果背景信息中包含工具引用(@工具名),请优先使用这些工具。当有多个同类工具可选时(如多个搜索工具),优先选择背景信息中已使用的工具,避免功能重叠。`;
};
/**
* 合并用户 prompt 和可选运行时 prompt 片段,再执行主 Agent prompt 改写。
*/
export const buildAgentLoopCoreSystemPrompt = ({
userSystemPrompt,
runtimePrompts = []
}: {
userSystemPrompt?: string;
runtimePrompts?: string[];
}) =>
parseAgentLoopCoreUserSystemPrompt({
userSystemPrompt: [userSystemPrompt, ...runtimePrompts].filter(Boolean).join('\n\n')
});
...@@ -11,7 +11,6 @@ export { ...@@ -11,7 +11,6 @@ export {
} from '../application/context/files'; } from '../application/context/files';
export { readAgentLoopCoreActivePlan } from '../application/context/activePlan'; export { readAgentLoopCoreActivePlan } from '../application/context/activePlan';
export { buildAgentLoopCoreRequestMessages } from '../application/context/messages'; export { buildAgentLoopCoreRequestMessages } from '../application/context/messages';
export { buildAgentLoopCoreSystemPrompt } from '../application/context/prompt';
export { export {
buildAgentLoopCoreUserReminderInput, buildAgentLoopCoreUserReminderInput,
type AgentLoopCoreUserReminderContext, type AgentLoopCoreUserReminderContext,
......
...@@ -2,6 +2,7 @@ import type { ...@@ -2,6 +2,7 @@ import type {
ChatCompletionMessageParam, ChatCompletionMessageParam,
CompletionFinishReason CompletionFinishReason
} from '@fastgpt/global/core/ai/llm/type'; } from '@fastgpt/global/core/ai/llm/type';
import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants';
import type { DispatchToolModuleProps } from './type'; import type { DispatchToolModuleProps } from './type';
import type { AIChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import type { AIChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { normalizeAgentLoopUsages } from '../../../../ai/llm/agentLoop/interface'; import { normalizeAgentLoopUsages } from '../../../../ai/llm/agentLoop/interface';
...@@ -87,12 +88,24 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo ...@@ -87,12 +88,24 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
cacheToolFlowResponse: runtimeEnvironment.cacheToolFlowResponse cacheToolFlowResponse: runtimeEnvironment.cacheToolFlowResponse
}); });
getProviderToolInfo = toolProvider.getToolInfo; getProviderToolInfo = toolProvider.getToolInfo;
const systemPrompt = toolProvider.finalMessages
.filter((message) => message.role === ChatCompletionRequestMessageRoleEnum.System)
.flatMap((message) => {
if (typeof message.content === 'string') return [message.content];
return (message.content ?? []).flatMap((item) => (item.type === 'text' ? [item.text] : []));
})
.filter(Boolean)
.join('\n\n');
const loopMessages = toolProvider.finalMessages.filter(
(message) => message.role !== ChatCompletionRequestMessageRoleEnum.System
);
const { summary: outputSummary } = const { summary: outputSummary } =
await runAgentLoopCoreWithSummary<WorkflowInteractiveResponseType>({ await runAgentLoopCoreWithSummary<WorkflowInteractiveResponseType>({
provider: 'fastAgent', provider: 'fastAgent',
input: buildAgentLoopCoreInput({ input: buildAgentLoopCoreInput({
messages: toolProvider.finalMessages, messages: loopMessages,
systemPrompt,
childrenInteractiveParams childrenInteractiveParams
}), }),
runtime: createAgentLoopCoreRuntimeWithEnvironment({ runtime: createAgentLoopCoreRuntimeWithEnvironment({
...@@ -100,7 +113,6 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo ...@@ -100,7 +113,6 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
environment: runtimeEnvironment, environment: runtimeEnvironment,
llmParams: { llmParams: {
model: toolModel.model, model: toolModel.model,
promptMode: 'raw',
maxTokens: maxToken, maxTokens: maxToken,
stream, stream,
temperature, temperature,
......
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AuxiliaryGenerationEventEnum } from '@fastgpt/global/core/ai/auxiliaryGeneration/constants';
const { runAgentLoopMock } = vi.hoisted(() => ({
runAgentLoopMock: vi.fn()
}));
vi.mock('@fastgpt/service/core/ai/llm/agentLoop/interface', async (importOriginal) => ({
...(await importOriginal()),
runAgentLoop: runAgentLoopMock
}));
import { runAuxiliaryGenerationAgentLoop } from '@fastgpt/service/core/ai/auxiliaryGeneration/agentLoop';
describe('runAuxiliaryGenerationAgentLoop', () => {
beforeEach(() => {
runAgentLoopMock.mockReset();
});
it('uses ask without plan and preserves pause state', async () => {
const streamWriter = vi.fn();
const executeTool = vi.fn();
const providerState = { pendingMainContext: { askToolCallId: 'ask_1' } };
const nextProviderState = { pendingMainContext: { askToolCallId: 'ask_2' } };
runAgentLoopMock.mockImplementation(async ({ runtime }) => {
runtime.emitEvent({ type: 'reasoning_delta', text: '分析中' });
runtime.emitEvent({ type: 'answer_delta', text: '处理中' });
return {
status: 'paused',
pause: {
type: 'ask',
askId: 'ask_2',
ask: {
reason: '需要选择',
blockerType: 'user_choice',
questions: [
{
question: '选择范围?',
options: [
{ summary: '小', value: '小范围' },
{ summary: '大', value: '大范围' }
]
}
]
}
},
providerState: nextProviderState,
completeMessages: [],
assistantMessages: [
{ role: 'assistant', reasoning_content: '分析中' },
{ role: 'assistant', content: '处理中' }
],
requestIds: [],
finishReason: 'tool_calls',
usages: []
};
});
const result = await runAuxiliaryGenerationAgentLoop({
teamId: 'team_1',
model: 'gpt-4o',
systemPrompt: 'helper prompt',
messages: [{ role: 'user', content: '创建客服 Agent' }],
providerState,
userAnswer: JSON.stringify({ answers: ['小范围'] }),
runtimeTools: [
{
type: 'function',
function: {
name: 'generate_config',
description: 'Generate config',
parameters: { type: 'object', properties: {} }
}
}
],
executeTool,
streamWriter
});
expect(runAgentLoopMock).toHaveBeenCalledWith(
expect.objectContaining({
runtime: expect.objectContaining({
systemTools: {
ask: { enabled: true }
},
toolCatalog: expect.objectContaining({
runtimeTools: expect.arrayContaining([
expect.objectContaining({
function: expect.objectContaining({ name: 'generate_config' })
})
])
}),
executeTool
}),
input: {
systemPrompt: 'helper prompt',
messages: [{ role: 'user', content: '创建客服 Agent' }],
providerState,
userAnswer: JSON.stringify({ answers: ['小范围'] })
}
})
);
expect(runAgentLoopMock.mock.calls[0][0].runtime.systemTools.plan).toBeUndefined();
expect(result).toEqual(
expect.objectContaining({
status: 'paused',
pause: expect.objectContaining({ askId: 'ask_2' }),
providerState: nextProviderState,
answerText: '处理中',
reasoningText: '分析中'
})
);
expect(streamWriter).toHaveBeenCalledWith(
expect.objectContaining({ event: AuxiliaryGenerationEventEnum.answer })
);
});
});
...@@ -68,6 +68,7 @@ vi.mock('@fastgpt/service/core/ai/sandbox/interface/toolCall', () => ({ ...@@ -68,6 +68,7 @@ vi.mock('@fastgpt/service/core/ai/sandbox/interface/toolCall', () => ({
})); }));
import { import {
buildDefaultAgentSystemPrompt,
createAskUserAgentTool, createAskUserAgentTool,
createSetPlanAgentTool, createSetPlanAgentTool,
createUpdatePlanAgentTool createUpdatePlanAgentTool
...@@ -144,6 +145,7 @@ describe('runFastAgentMainLoop', () => { ...@@ -144,6 +145,7 @@ describe('runFastAgentMainLoop', () => {
const result = await runFastAgentMainLoop({ const result = await runFastAgentMainLoop({
runtime: createRuntime(), runtime: createRuntime(),
input: { input: {
systemPrompt: buildDefaultAgentSystemPrompt(),
messages: [ messages: [
{ {
role: ChatCompletionRequestMessageRoleEnum.User, role: ChatCompletionRequestMessageRoleEnum.User,
...@@ -159,64 +161,9 @@ describe('runFastAgentMainLoop', () => { ...@@ -159,64 +161,9 @@ describe('runFastAgentMainLoop', () => {
expect(result.activePlan).toBeUndefined(); expect(result.activePlan).toBeUndefined();
expect(createLLMResponseMock).toHaveBeenCalledTimes(1); expect(createLLMResponseMock).toHaveBeenCalledTimes(1);
const mainAgentPrompt = createLLMResponseMock.mock.calls[0][0].body.messages[0].content; const mainAgentPrompt = createLLMResponseMock.mock.calls[0][0].body.messages[0].content;
expect(mainAgentPrompt).toContain('你是 Work Agent'); expect(mainAgentPrompt).toContain('你是一个 Work Agent');
expect(mainAgentPrompt).toContain('任务或 Skill 明确需要通过选项向用户收集信息');
expect(mainAgentPrompt).toContain('Skill 要求向用户收集选项信息时');
expect(mainAgentPrompt).toContain('每个问题提供 2 到 4 个候选答案');
}); });
it.each([
{
name: 'no runtime-capable tools',
toolCatalog: { runtimeTools: [] },
expectedConstraint: true
},
{
name: 'sandbox tools only',
toolCatalog: { runtimeTools: [], sandboxTools: [tool('sandbox_shell')] },
expectedConstraint: false
},
{
name: 'read file tool only',
toolCatalog: { runtimeTools: [], readFileTool: tool('read_files') },
expectedConstraint: false
},
{
name: 'dataset search tool only',
toolCatalog: { runtimeTools: [], datasetSearchTool: tool('dataset_search') },
expectedConstraint: false
}
] satisfies Array<{
name: string;
toolCatalog: AgentLoopRuntime['toolCatalog'];
expectedConstraint: boolean;
}>)(
'sets the runtime tool constraint correctly with $name',
async ({ toolCatalog, expectedConstraint }) => {
mockCreateLLMResponseQueue(createLLMResponseMock, [
text({
requestId: 'req_runtime_tool_constraint',
content: 'direct answer'
})
]);
await runFastAgentMainLoop({
runtime: createRuntime({ toolCatalog }),
input: {
messages: [
{
role: ChatCompletionRequestMessageRoleEnum.User,
content: 'hello'
}
]
}
});
const mainAgentPrompt = createLLMResponseMock.mock.calls[0][0].body.messages[0].content;
expect(mainAgentPrompt.includes('<tool_constraint>')).toBe(expectedConstraint);
}
);
it('creates an active plan through set_plan', async () => { it('creates an active plan through set_plan', async () => {
const events: any[] = []; const events: any[] = [];
mockCreateLLMResponseQueue(createLLMResponseMock, [ mockCreateLLMResponseQueue(createLLMResponseMock, [
......
...@@ -104,6 +104,43 @@ describe('runFastAgentLoop', () => { ...@@ -104,6 +104,43 @@ describe('runFastAgentLoop', () => {
})); }));
}); });
it('uses the final input systemPrompt without injecting provider defaults', async () => {
mockCreateLLMResponseQueue(createLLMResponseMock, [
text({
requestId: 'req_system_prompt',
content: 'direct answer'
})
]);
await runFastAgentLoop({
input: {
systemPrompt: 'final system prompt',
messages: [
{
role: ChatCompletionRequestMessageRoleEnum.System,
content: 'stale system prompt'
},
{
role: ChatCompletionRequestMessageRoleEnum.User,
content: 'hello'
}
]
},
runtime: createRuntime()
});
expect(createLLMResponseMock.mock.calls[0][0].body.messages.slice(0, 2)).toEqual([
{
role: ChatCompletionRequestMessageRoleEnum.System,
content: 'final system prompt'
},
{
role: ChatCompletionRequestMessageRoleEnum.User,
content: 'hello'
}
]);
});
it('injects system tools only when runtime systemTools enable them', async () => { it('injects system tools only when runtime systemTools enable them', async () => {
mockCreateLLMResponseQueue(createLLMResponseMock, [ mockCreateLLMResponseQueue(createLLMResponseMock, [
text({ text({
......
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { getMainAgentSystemPrompt } from '@fastgpt/service/core/ai/llm/agentLoop/domain/mainPrompt'; import { buildDefaultAgentSystemPrompt } from '@fastgpt/service/core/ai/llm/agentLoop/interface';
describe('getMainAgentSystemPrompt', () => { describe('buildDefaultAgentSystemPrompt', () => {
it('describes the flat set_plan and update_plan arguments', () => { it('uses the fixed prompt without capability-specific rules', () => {
const prompt = getMainAgentSystemPrompt({ const prompt = buildDefaultAgentSystemPrompt();
systemPrompt: 'Follow project conventions.',
hasRuntimeTools: true expect(prompt).toContain('你是一个 Work Agent');
expect(prompt).toContain('不要调用不存在的工具');
expect(prompt).not.toContain('set_plan');
expect(prompt).not.toContain('update_plan');
expect(prompt).not.toContain('ask_user');
expect(prompt).not.toContain('可用工具');
});
it('injects the user-configured system prompt as a separate section', () => {
const prompt = buildDefaultAgentSystemPrompt({
userSystemPrompt: '优先使用 generate_config 完成配置生成。'
}); });
expect(prompt).toContain('<user_background>\nFollow project conventions.\n</user_background>');
expect(prompt).toContain( expect(prompt).toContain(
'set_plan,参数格式为 {"name":"简短计划名","steps":["步骤一","步骤二"]}' '<user_system_prompt>\n优先使用 generate_config 完成配置生成。\n</user_system_prompt>'
); );
expect(prompt).toContain( });
'update_plan,参数格式为 {"updates":[{"id":"已有步骤 id","status":"done","note":"简短结果"}]}'
it('keeps platform extensions outside the user system prompt section', () => {
const prompt = buildDefaultAgentSystemPrompt({
systemPromptExtension: '平台辅助生成规则',
userSystemPrompt: '用户配置规则'
});
const userPromptContent = prompt.match(
/<user_system_prompt>\n([\s\S]*?)\n<\/user_system_prompt>/
)?.[1];
expect(prompt).toContain('平台辅助生成规则');
expect(userPromptContent).toBe('用户配置规则');
});
it('ignores an empty caller system prompt', () => {
expect(buildDefaultAgentSystemPrompt({ userSystemPrompt: ' ' })).not.toContain(
'<user_system_prompt>'
); );
expect(prompt).toContain('update_plan,参数格式为 {"add_steps":["新增步骤"]}');
expect(prompt).toContain('不要传 action 或 description');
expect(prompt).toContain('不要把 updates 写成 steps');
expect(prompt).toContain('set_plan 必须是第一个工具调用');
expect(prompt).toContain('已有 active plan 时不要再次调用 set_plan');
}); });
it('adds the runtime tool constraint only when runtime tools are unavailable', () => { it('injects sandbox capability as a default prompt section without mixing it into user prompt', () => {
expect(getMainAgentSystemPrompt({ systemPrompt: undefined, hasRuntimeTools: false })).toContain( const userSystemPrompt = '只处理用户配置的要求。';
'<tool_constraint>' const prompt = buildDefaultAgentSystemPrompt({
userSystemPrompt,
sandboxEnabled: true
});
const userPromptSection = `<user_system_prompt>\n${userSystemPrompt}\n</user_system_prompt>`;
const userPromptContent = prompt.match(
/<user_system_prompt>\n([\s\S]*?)\n<\/user_system_prompt>/
)?.[1];
expect(prompt).toContain('<sandbox_capability>');
expect(prompt).toContain('</sandbox_capability>');
expect(prompt.indexOf('<sandbox_capability>')).toBeLessThan(prompt.indexOf(userPromptSection));
expect(userPromptContent).toBe(userSystemPrompt);
});
it('omits sandbox capability when disabled', () => {
expect(buildDefaultAgentSystemPrompt({ sandboxEnabled: false })).not.toContain(
'<sandbox_capability>'
); );
expect(
getMainAgentSystemPrompt({ systemPrompt: undefined, hasRuntimeTools: true })
).not.toContain('<tool_constraint>');
}); });
}); });
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.schema'; import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.schema';
import { ModelTypeEnum } from '@fastgpt/global/core/ai/constants'; import { ModelTypeEnum } from '@fastgpt/global/core/ai/constants';
import type { AgentLoopSystemTools } from '@fastgpt/service/core/ai/llm/agentLoop/domain';
const { const {
agentPromptMock, agentPromptMock,
...@@ -303,15 +302,7 @@ describe('runPiAgentLoop', () => { ...@@ -303,15 +302,7 @@ describe('runPiAgentLoop', () => {
}); });
expect(agentPromptMock).toHaveBeenCalledWith('hello'); expect(agentPromptMock).toHaveBeenCalledWith('hello');
expect(agentConstructorArgs[0].initialState.systemPrompt).toContain( expect(agentConstructorArgs[0].initialState.systemPrompt).toBe('system prompt');
'<user_background>\nsystem prompt\n</user_background>'
);
expect(agentConstructorArgs[0].initialState.systemPrompt).toContain(
'set_plan,参数格式为 {"name":"简短计划名","steps":["步骤一","步骤二"]}'
);
expect(agentConstructorArgs[0].initialState.systemPrompt).toContain(
'update_plan,参数格式为 {"updates":[{"id":"已有步骤 id","status":"done","note":"简短结果"}]}'
);
expect(agentConstructorArgs[0].toolExecution).toBe('sequential'); expect(agentConstructorArgs[0].toolExecution).toBe('sequential');
expect(agentConstructorArgs[0].initialState.messages).toEqual([]); expect(agentConstructorArgs[0].initialState.messages).toEqual([]);
expect(result).toMatchObject({ expect(result).toMatchObject({
...@@ -367,73 +358,6 @@ describe('runPiAgentLoop', () => { ...@@ -367,73 +358,6 @@ describe('runPiAgentLoop', () => {
]); ]);
}); });
it.each([
{
name: 'no runtime-capable tools',
systemTools: undefined,
expectedConstraint: true
},
{
name: 'sandbox tools only',
systemTools: {
sandbox: {
enabled: true,
client: {} as any
}
},
expectedConstraint: false
},
{
name: 'read file tool only',
systemTools: {
readFile: {
enabled: true,
maxFileAmount: 20,
execute: vi.fn()
}
},
expectedConstraint: false
},
{
name: 'dataset search tool only',
systemTools: {
datasetSearch: {
enabled: true,
execute: vi.fn()
}
},
expectedConstraint: false
}
] satisfies Array<{
name: string;
systemTools?: AgentLoopSystemTools;
expectedConstraint: boolean;
}>)(
'sets the runtime tool constraint correctly with $name',
async ({ systemTools, expectedConstraint }) => {
await runPiAgentLoop({
input: {
messages: [{ role: 'user', content: 'hello' }]
},
runtime: {
llmParams: {
model: 'gpt-5'
},
systemTools,
toolCatalog: {
runtimeTools: []
},
executeTool: vi.fn(),
checkIsStopping: vi.fn(() => false)
}
});
expect(
agentConstructorArgs.at(-1).initialState.systemPrompt.includes('<tool_constraint>')
).toBe(expectedConstraint);
}
);
it('preserves multimodal content in the current user prompt', async () => { it('preserves multimodal content in the current user prompt', async () => {
await runPiAgentLoop({ await runPiAgentLoop({
input: { input: {
......
...@@ -432,7 +432,7 @@ describe('dispatchRunAgent user context', () => { ...@@ -432,7 +432,7 @@ describe('dispatchRunAgent user context', () => {
expect(systemPrompt).not.toContain('{{@mcp-app_1/search@}}'); expect(systemPrompt).not.toContain('{{@mcp-app_1/search@}}');
}); });
it('uses an empty system prompt when the parameter is omitted', async () => { it('uses the default agent system prompt when the user prompt is omitted', async () => {
const props = createProps(); const props = createProps();
delete props.params.systemPrompt; delete props.params.systemPrompt;
...@@ -449,7 +449,9 @@ describe('dispatchRunAgent user context', () => { ...@@ -449,7 +449,9 @@ describe('dispatchRunAgent user context', () => {
const result = await resultPromise!; const result = await resultPromise!;
expect(result.error).toBeUndefined(); expect(result.error).toBeUndefined();
expect(runAgentLoopMock.mock.calls[0][0].input.systemPrompt).toBe(''); const systemPrompt = runAgentLoopMock.mock.calls[0][0].input.systemPrompt;
expect(systemPrompt).toContain('你是一个 Work Agent。');
expect(systemPrompt).not.toContain('<user_system_prompt>');
}); });
it('injects sandbox input files before starting the agent loop', async () => { it('injects sandbox input files before starting the agent loop', async () => {
...@@ -499,6 +501,8 @@ describe('dispatchRunAgent user context', () => { ...@@ -499,6 +501,8 @@ describe('dispatchRunAgent user context', () => {
sandboxWriteFilesMock.mock.invocationCallOrder[0] sandboxWriteFilesMock.mock.invocationCallOrder[0]
); );
const loopInput = runAgentLoopMock.mock.calls[0][0].input; const loopInput = runAgentLoopMock.mock.calls[0][0].input;
expect(loopInput.systemPrompt).toContain('<sandbox_capability>');
expect(loopInput.systemPrompt).toContain('</sandbox_capability>');
expect(loopInput.systemPrompt).not.toContain('pwd: /workspace'); expect(loopInput.systemPrompt).not.toContain('pwd: /workspace');
expect(getMessageTextForTest(loopInput.messages.at(-1)?.content)).toContain( expect(getMessageTextForTest(loopInput.messages.at(-1)?.content)).toContain(
'当前 sandbox 工作目录: /workspace' '当前 sandbox 工作目录: /workspace'
...@@ -1209,7 +1213,7 @@ describe('dispatchRunAgent user context', () => { ...@@ -1209,7 +1213,7 @@ describe('dispatchRunAgent user context', () => {
expect(runAgentLoopMock).toHaveBeenCalledOnce(); expect(runAgentLoopMock).toHaveBeenCalledOnce();
const loopInput = runAgentLoopMock.mock.calls[0][0].input; const loopInput = runAgentLoopMock.mock.calls[0][0].input;
expect(loopInput.systemPrompt).not.toContain('## 沙盒能力'); expect(loopInput.systemPrompt).not.toContain('<sandbox_capability>');
expect(getMessageTextForTest(loopInput.messages.at(-1)?.content)).not.toContain( expect(getMessageTextForTest(loopInput.messages.at(-1)?.content)).not.toContain(
'<available_skills>' '<available_skills>'
); );
......
...@@ -5,9 +5,7 @@ import { ...@@ -5,9 +5,7 @@ import {
buildAgentLoopCoreInput, buildAgentLoopCoreInput,
buildAgentLoopCoreRequestMessages, buildAgentLoopCoreRequestMessages,
buildAgentLoopCoreSkillsPrompt, buildAgentLoopCoreSkillsPrompt,
buildAgentLoopCoreSystemPrompt, buildAgentLoopCoreUserReminderInput
buildAgentLoopCoreUserReminderInput,
parseAgentLoopCoreUserSystemPrompt
} from '@fastgpt/service/core/workflow/dispatch/ai/agentLoopCore/application/context'; } from '@fastgpt/service/core/workflow/dispatch/ai/agentLoopCore/application/context';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
...@@ -141,21 +139,6 @@ describe('buildAgentLoopCoreInput', () => { ...@@ -141,21 +139,6 @@ describe('buildAgentLoopCoreInput', () => {
}); });
}); });
describe('agentLoopCore prompt helpers', () => {
it('rewrites user system prompt and merges runtime prompts before rewriting', () => {
expect(parseAgentLoopCoreUserSystemPrompt({ userSystemPrompt: '' })).toBe('');
const prompt = buildAgentLoopCoreSystemPrompt({
userSystemPrompt: 'system prompt',
runtimePrompts: ['sandbox prompt']
});
expect(prompt).toContain('system prompt\n\nsandbox prompt');
expect(prompt).toContain('<user_background></user_background>');
expect(prompt).toContain('@工具名');
});
});
describe('agentLoopCore reminder helpers', () => { describe('agentLoopCore reminder helpers', () => {
it('builds escaped file and skill reminder blocks', () => { it('builds escaped file and skill reminder blocks', () => {
const filePrompt = buildAgentLoopCoreInputFilesPrompt([ const filePrompt = buildAgentLoopCoreInputFilesPrompt([
......
...@@ -188,6 +188,41 @@ describe('runToolCall compression node responses', () => { ...@@ -188,6 +188,41 @@ describe('runToolCall compression node responses', () => {
delete (global as any).feConfigs; delete (global as any).feConfigs;
}); });
it('passes the extracted system prompt separately from conversation messages', async () => {
runAgentLoopMock.mockResolvedValue(createLoopResult());
await runToolCall(
createProps({
messages: [
{
role: ChatCompletionRequestMessageRoleEnum.System,
content: 'custom system prompt'
},
{
role: ChatCompletionRequestMessageRoleEnum.System,
content: [{ type: 'text', text: 'array system prompt' }]
},
{
role: ChatCompletionRequestMessageRoleEnum.User,
content: 'hello'
}
]
})
);
expect(runAgentLoopMock.mock.calls[0][0].input).toEqual(
expect.objectContaining({
systemPrompt: 'custom system prompt\n\narray system prompt',
messages: [
{
role: ChatCompletionRequestMessageRoleEnum.User,
content: 'hello'
}
]
})
);
});
it('records context compression as ToolCall child node response and tool-response compression under the tool node', async () => { it('records context compression as ToolCall child node response and tool-response compression under the tool node', async () => {
const contextCompressUsage = { const contextCompressUsage = {
moduleName: 'account_usage:compress_llm_messages', moduleName: 'account_usage:compress_llm_messages',
...@@ -271,7 +306,6 @@ describe('runToolCall compression node responses', () => { ...@@ -271,7 +306,6 @@ describe('runToolCall compression node responses', () => {
}, },
llmParams: expect.objectContaining({ llmParams: expect.objectContaining({
model: 'gpt-4', model: 'gpt-4',
promptMode: 'raw',
maxTokens: 1000, maxTokens: 1000,
temperature: 0, temperature: 0,
topP: 0.7, topP: 0.7,
......
Subproject commit 9a73980b9ae674d91c21b075e7dc518679ae0003 Subproject commit 2bc391e398ec4da405442a47416291fa440129c2
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment