Commit 9f2adcd5 by Archer Committed by GitHub

perf: request llm (#6191)

* perf: request error info

* perf: request llm'

* perf: request llm'

* openapi doc
parent f7e46ec7
......@@ -20,6 +20,7 @@ description: 'FastGPT V4.14.5 更新说明'
2. MongoDB, Redis 和 MQ 的重连逻辑优化。
3. 变量输入框禁用状态可复制。
4. LLM 请求空响应判断,排除敏感过滤错误被误认为无响应。
5. 完善 AI 对话和工具调用的错误提示,提供更多原始数据。
## 🐛 修复
......
......@@ -120,7 +120,7 @@
"document/content/docs/upgrading/4-14/4142.mdx": "2025-11-18T19:27:14+08:00",
"document/content/docs/upgrading/4-14/4143.mdx": "2025-11-26T20:52:05+08:00",
"document/content/docs/upgrading/4-14/4144.mdx": "2025-12-16T14:56:04+08:00",
"document/content/docs/upgrading/4-14/4145.mdx": "2026-01-05T13:44:33+08:00",
"document/content/docs/upgrading/4-14/4145.mdx": "2026-01-05T15:39:04+08:00",
"document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00",
......
......@@ -41,6 +41,7 @@ export enum EmbeddingTypeEnm {
}
export const completionFinishReasonMap = {
error: i18nT('chat:completion_finish_error'),
close: i18nT('chat:completion_finish_close'),
stop: i18nT('chat:completion_finish_stop'),
length: i18nT('chat:completion_finish_length'),
......
import openai from 'openai';
import type {
ChatCompletion as SdkChatCompletion,
ChatCompletionMessageToolCall,
ChatCompletionMessageParam as SdkChatCompletionMessageParam,
ChatCompletionToolMessageParam,
......@@ -70,10 +71,16 @@ export type ChatCompletionMessageFunctionCall =
};
// Stream response
export type StreamChatType = Stream<openai.Chat.Completions.ChatCompletionChunk>;
export type StreamChatType = Stream<openai.Chat.Completions.ChatCompletionChunk & { error?: any }>;
export type UnStreamChatType = openai.Chat.Completions.ChatCompletion;
// UnStream response
export type ChatCompletion = SdkChatCompletion & {
error?: any;
};
export type CompletionFinishReason =
| 'error'
| 'close'
| 'stop'
| 'length'
......
......@@ -2,6 +2,31 @@ import { OutLinkChatAuthSchema } from '../../../../support/permission/chat';
import { ObjectIdSchema } from '../../../../common/type/mongo';
import z from 'zod';
/* Init */
// Online chat
export const InitChatQuerySchema = z
.object({
appId: ObjectIdSchema.describe('应用ID'),
chatId: z.string().min(1).describe('对话ID'),
loadCustomFeedbacks: z.boolean().optional().describe('是否加载自定义反馈')
})
.meta({
example: {
appId: '1234567890',
chatId: '1234567890',
loadCustomFeedbacks: true
}
});
export type InitChatQueryType = z.infer<typeof InitChatQuerySchema>;
export const InitChatResponseSchema = z.object({
chatId: z.string().min(1).describe('对话ID'),
appId: ObjectIdSchema.describe('应用ID'),
userAvatar: z.string().optional().describe('用户头像'),
title: z.string().min(1).describe('对话标题'),
variables: z.record(z.string(), z.any()).optional().describe('全局变量值'),
app: z.object({}).describe('应用配置')
});
/* ============ v2/chat/stop ============ */
export const StopV2ChatSchema = z
.object({
......
......@@ -28,7 +28,13 @@ export const openAPIDocument = createDocument({
},
{
name: '对话管理',
tags: [TagsMap.chatHistory, TagsMap.chatPage, TagsMap.chatFeedback, TagsMap.chatSetting]
tags: [
TagsMap.chatPage,
TagsMap.chatHistory,
TagsMap.chatController,
TagsMap.chatFeedback,
TagsMap.chatSetting
]
},
{
name: '知识库',
......
......@@ -6,11 +6,11 @@ export const TagsMap = {
appCommon: 'Agent 管理',
// Chat - home
chatPage: '对话页',
chatController: '对话框操作',
chatHistory: '对话历史管理',
chatSetting: '门户页配置',
chatPage: '对话页面通用',
chatHistory: '历史记录管理',
chatController: '对话操作',
chatFeedback: '对话反馈',
chatSetting: '门户页配置',
// Dataset
datasetCollection: '集合',
......
......@@ -55,6 +55,7 @@ type RunAgentCallProps = {
} & ResponseEvents;
type RunAgentResponse = {
error?: any;
completeMessages: ChatCompletionMessageParam[]; // Step request complete messages
assistantMessages: ChatCompletionMessageParam[]; // Step assistant response messages
interactiveResponse?: ToolCallChildrenInteractive;
......@@ -134,6 +135,7 @@ export const runAgentCall = async ({
let inputTokens: number = 0;
let outputTokens: number = 0;
let finish_reason: CompletionFinishReason | undefined;
let requestError: any;
const subAppUsages: ChatNodeUsageType[] = [];
// 处理 tool 里的交互
......@@ -213,8 +215,10 @@ export const runAgentCall = async ({
usage,
responseEmptyTip,
assistantMessage: llmAssistantMessage,
finish_reason: finishReason
finish_reason: finishReason,
error
} = await createLLMResponse({
throwError: false,
body: {
...body,
max_tokens: maxTokens,
......@@ -234,7 +238,11 @@ export const runAgentCall = async ({
});
finish_reason = finishReason;
requestError = error;
if (requestError) {
break;
}
if (responseEmptyTip) {
return Promise.reject(responseEmptyTip);
}
......@@ -303,6 +311,7 @@ export const runAgentCall = async ({
}
return {
error: requestError,
inputTokens,
outputTokens,
subAppUsages,
......
......@@ -364,6 +364,9 @@ export const loadRequestMessages = async ({
const loadMessages = (
await Promise.all(
mergeMessages.map(async (item, i) => {
delete item.dataId;
delete item.hideInUI;
if (item.role === ChatCompletionRequestMessageRoleEnum.System) {
const content = parseSystemMessage(item.content);
if (!content) return;
......
......@@ -73,6 +73,7 @@ export const parseLLMStreamResponse = () => {
let buffer_usage: CompletionUsage = getLLMDefaultUsage();
let buffer_reasoningContent = '';
let buffer_content = '';
let error: any = undefined;
/*
parseThinkTag - 只控制是否主动解析 <think></think>,如果接口已经解析了,则不再解析。
......@@ -84,6 +85,7 @@ export const parseLLMStreamResponse = () => {
retainDatasetCite = true
}: {
part: {
error?: any;
choices: {
delta: {
content?: string | null;
......@@ -96,6 +98,7 @@ export const parseLLMStreamResponse = () => {
parseThinkTag?: boolean;
retainDatasetCite?: boolean;
}): {
error?: any;
reasoningContent: string;
content: string; // 原始内容,不去掉 cite
responseContent: string; // 响应的内容,会去掉 cite
......@@ -297,11 +300,14 @@ export const parseLLMStreamResponse = () => {
buffer_reasoningContent += data.reasoningContent;
buffer_content += data.content;
error = part.error || error;
return data;
};
const getResponseData = () => {
return {
error,
finish_reason: buffer_finishReason,
usage: buffer_usage,
reasoningContent: buffer_reasoningContent,
......@@ -312,11 +318,15 @@ export const parseLLMStreamResponse = () => {
const updateFinishReason = (finishReason: CompletionFinishReason) => {
buffer_finishReason = finishReason;
};
const updateError = (err: any) => {
error = err;
};
return {
parsePart,
getResponseData,
updateFinishReason
updateFinishReason,
updateError
};
};
......
......@@ -177,47 +177,55 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
const write = res ? responseWriteController({ res, readStream: stream }) : undefined;
const { completeMessages, reasoningText, answerText, finish_reason, responseEmptyTip, usage } =
await createLLMResponse({
body: {
model: modelConstantsData.model,
stream,
messages: filterMessages,
temperature,
max_tokens,
top_p: aiChatTopP,
stop: aiChatStopSign,
response_format: {
type: aiChatResponseFormat,
json_schema: aiChatJsonSchema
},
retainDatasetCite,
useVision: aiChatVision,
requestOrigin
const {
completeMessages,
reasoningText,
answerText,
finish_reason,
responseEmptyTip,
usage,
error
} = await createLLMResponse({
throwError: false,
body: {
model: modelConstantsData.model,
stream,
messages: filterMessages,
temperature,
max_tokens,
top_p: aiChatTopP,
stop: aiChatStopSign,
response_format: {
type: aiChatResponseFormat,
json_schema: aiChatJsonSchema
},
userKey: externalProvider.openaiAccount,
isAborted: checkIsStopping,
onReasoning({ text }) {
if (!aiChatReasoning) return;
workflowStreamResponse?.({
write,
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
reasoning_content: text
})
});
},
onStreaming({ text }) {
if (!isResponseAnswerText) return;
workflowStreamResponse?.({
write,
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
text
})
});
}
});
retainDatasetCite,
useVision: aiChatVision,
requestOrigin
},
userKey: externalProvider.openaiAccount,
isAborted: checkIsStopping,
onReasoning({ text }) {
if (!aiChatReasoning) return;
workflowStreamResponse?.({
write,
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
reasoning_content: text
})
});
},
onStreaming({ text }) {
if (!isResponseAnswerText) return;
workflowStreamResponse?.({
write,
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
text
})
});
}
});
if (responseEmptyTip) {
return getNodeErrResponse({ error: responseEmptyTip });
......@@ -232,6 +240,35 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
const chatCompleteMessages = GPTMessages2Chats({ messages: completeMessages });
if (error) {
return getNodeErrResponse({
error,
responseData: {
totalPoints: points,
model: modelName,
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
query: `${userChatInput}`,
maxToken: max_tokens,
reasoningText,
historyPreview: getHistoryPreview(chatCompleteMessages, 10000, aiChatVision),
contextTotalLen: completeMessages.length,
finishReason: finish_reason
},
...(points && {
[DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [
{
moduleName: name,
totalPoints: points,
model: modelName,
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens
}
]
})
});
}
return {
data: {
answerText: answerText,
......
......@@ -14,7 +14,6 @@ import { formatModelChars2Points } from '../../../../support/wallet/usage/utils'
import { type DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { addLog } from '../../../../common/system/log';
import { ModelTypeEnum } from '../../../../../global/core/ai/model';
import { createLLMResponse } from '../../../ai/llm/request';
type Props = ModuleDispatchProps<{
......
......@@ -187,7 +187,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
toolCallOutputTokens,
completeMessages = [], // The actual message sent to AI(just save text)
assistantResponses = [], // FastGPT system store assistant.value response
finish_reason
finish_reason,
error
} = await (async () => {
const adaptMessages = chats2GPTMessages({
messages,
......@@ -224,6 +225,46 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
// Preview assistant responses
const previewAssistantResponses = filterToolResponseToPreview(assistantResponses);
if (error) {
return getNodeErrResponse({
error,
[DispatchNodeResponseKeyEnum.nodeResponse]: {
totalPoints: totalPointsUsage,
toolCallInputTokens: toolCallInputTokens,
toolCallOutputTokens: toolCallOutputTokens,
childTotalPoints: toolTotalPoints,
model: modelName,
query: userChatInput,
historyPreview: getHistoryPreview(
GPTMessages2Chats({ messages: completeMessages, reserveTool: false }),
10000,
useVision
),
toolDetail: toolDispatchFlowResponses.map((item) => item.flowResponses).flat(),
mergeSignId: nodeId,
finishReason: finish_reason
},
[DispatchNodeResponseKeyEnum.runTimes]: toolDispatchFlowResponses.reduce(
(sum, item) => sum + item.runTimes,
0
),
...(totalPointsUsage && {
[DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [
// 模型本身的积分消耗
{
moduleName: name,
model: modelName,
totalPoints: modelUsage,
inputTokens: toolCallInputTokens,
outputTokens: toolCallOutputTokens
},
// 工具的消耗
...toolUsages
]
})
});
}
return {
data: {
[NodeOutputKeyEnum.answerText]: previewAssistantResponses
......
......@@ -110,7 +110,8 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<RunTo
completeMessages,
assistantMessages,
interactiveResponse,
finish_reason
finish_reason,
error
} = await runAgentCall({
maxRunAgentTimes: 50,
body: {
......@@ -310,6 +311,7 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<RunTo
.flat();
return {
error,
toolDispatchFlowResponses: toolRunResponses,
toolCallInputTokens: inputTokens,
toolCallOutputTokens: outputTokens,
......
......@@ -46,6 +46,7 @@ export type DispatchToolModuleProps = ModuleDispatchProps<{
};
export type RunToolResponse = {
error?: any;
toolDispatchFlowResponses: DispatchFlowResponse[];
toolCallInputTokens: number;
toolCallOutputTokens: number;
......
......@@ -329,7 +329,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
return getNodeErrResponse({
error,
customNodeResponse: {
[DispatchNodeResponseKeyEnum.nodeResponse]: {
toolInput,
moduleLogo: avatar
}
......
......@@ -203,6 +203,9 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
: null
};
} catch (error) {
return getNodeErrResponse({ error, customNodeResponse: { moduleLogo: plugin?.avatar } });
return getNodeErrResponse({
error,
[DispatchNodeResponseKeyEnum.nodeResponse]: { moduleLogo: plugin?.avatar }
});
}
};
......@@ -25,6 +25,7 @@ import { getMCPChildren } from '../../../core/app/mcp';
import { getSystemToolRunTimeNodeFromSystemToolset } from '../utils';
import type { localeType } from '@fastgpt/global/common/i18n/type';
import type { HttpToolConfigType } from '@fastgpt/global/core/app/type';
import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
export const getWorkflowResponseWrite = ({
res,
......@@ -293,22 +294,34 @@ export const rewriteRuntimeWorkFlow = async ({
export const getNodeErrResponse = ({
error,
customErr,
customNodeResponse
responseData,
nodeDispatchUsages,
runTimes,
newVariables,
system_memories
}: {
error: any;
customErr?: Record<string, any>;
customNodeResponse?: Record<string, any>;
[DispatchNodeResponseKeyEnum.nodeResponse]?: Record<string, any>;
[DispatchNodeResponseKeyEnum.nodeDispatchUsages]?: ChatNodeUsageType[]; // Node total usage
[DispatchNodeResponseKeyEnum.runTimes]?: number;
[DispatchNodeResponseKeyEnum.newVariables]?: Record<string, any>;
[DispatchNodeResponseKeyEnum.memories]?: Record<string, any>;
}) => {
const errorText = getErrText(error);
return {
[DispatchNodeResponseKeyEnum.nodeDispatchUsages]: nodeDispatchUsages,
[DispatchNodeResponseKeyEnum.runTimes]: runTimes,
[DispatchNodeResponseKeyEnum.newVariables]: newVariables,
[DispatchNodeResponseKeyEnum.memories]: system_memories,
error: {
[NodeOutputKeyEnum.errorText]: errorText,
...(typeof customErr === 'object' ? customErr : {})
},
[DispatchNodeResponseKeyEnum.nodeResponse]: {
errorText,
...(typeof customNodeResponse === 'object' ? customNodeResponse : {})
...(typeof responseData === 'object' ? responseData : {})
},
[DispatchNodeResponseKeyEnum.toolResponses]: {
error: errorText,
......
......@@ -19,6 +19,7 @@
"click_to_add_url": "Enter file link",
"completion_finish_close": "Disconnection",
"completion_finish_content_filter": "Trigger safe wind control",
"completion_finish_error": "Request error",
"completion_finish_function_call": "Function Calls",
"completion_finish_length": "Reply limit exceeded",
"completion_finish_null": "unknown",
......
......@@ -19,6 +19,7 @@
"click_to_add_url": "输入文件链接",
"completion_finish_close": "请求关闭",
"completion_finish_content_filter": "触发安全风控",
"completion_finish_error": "请求错误",
"completion_finish_function_call": "函数调用",
"completion_finish_length": "超出回复限制",
"completion_finish_null": "未知",
......
......@@ -19,6 +19,7 @@
"click_to_add_url": "輸入文件鏈接",
"completion_finish_close": "連接斷開",
"completion_finish_content_filter": "觸發安全風控",
"completion_finish_error": "請求錯誤",
"completion_finish_function_call": "函式呼叫",
"completion_finish_length": "超出回覆限制",
"completion_finish_null": "未知",
......
......@@ -149,6 +149,18 @@ export const WholeResponseContent = ({
value={formatNumber(activeModule.childTotalPoints)}
/>
)}
<Row label={t('workflow:response.Error')} value={activeModule?.error} />
<Row label={t('workflow:response.Error')} value={activeModule?.errorText} />
<Row label={t('chat:response.node_inputs')} value={activeModule?.nodeInputs} />
</>
{/* ai chat */}
<>
{activeModule?.finishReason && (
<Row
label={t('chat:completion_finish_reason')}
value={t(completionFinishReasonMap[activeModule?.finishReason])}
/>
)}
<Row label={t('common:core.chat.response.module model')} value={activeModule?.model} />
{activeModule?.tokens && (
<Row label={t('chat:llm_tokens')} value={`${activeModule?.tokens}`} />
......@@ -171,12 +183,6 @@ export const WholeResponseContent = ({
label={t('common:core.chat.response.context total length')}
value={activeModule?.contextTotalLen}
/>
<Row label={t('workflow:response.Error')} value={activeModule?.error} />
<Row label={t('workflow:response.Error')} value={activeModule?.errorText} />
<Row label={t('chat:response.node_inputs')} value={activeModule?.nodeInputs} />
</>
{/* ai chat */}
<>
<Row
label={t('common:core.chat.response.module temperature')}
value={activeModule?.temperature}
......@@ -185,12 +191,6 @@ export const WholeResponseContent = ({
label={t('common:core.chat.response.module maxToken')}
value={activeModule?.maxToken}
/>
{activeModule?.finishReason && (
<Row
label={t('chat:completion_finish_reason')}
value={t(completionFinishReasonMap[activeModule?.finishReason])}
/>
)}
<Row label={t('chat:reasoning_text')} value={activeModule?.reasoningText} />
<Row
......
......@@ -15,11 +15,6 @@ export type GetChatSpeechProps = OutLinkChatAuthProps & {
};
/* ---------- chat ----------- */
export type InitChatProps = {
appId?: string;
chatId?: string;
loadCustomFeedbacks?: boolean;
};
export type GetChatRecordsProps = OutLinkChatAuthProps & {
appId: string;
......
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { getGuideModule, getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { getChatModelNameListByModules } from '@/service/core/app/workflow';
import type { InitChatProps, InitChatResponse } from '@/global/core/chat/api.d';
import type { InitChatResponse } from '@/global/core/chat/api.d';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller';
......@@ -14,19 +13,10 @@ import { presignVariablesFileUrls } from '@fastgpt/service/core/chat/utils';
import { MongoAppRecord } from '@fastgpt/service/core/app/record/schema';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { InitChatQuerySchema } from '@fastgpt/global/openapi/core/chat/controler/api';
async function handler(
req: NextApiRequest,
res: NextApiResponse
): Promise<InitChatResponse | void> {
let { appId, chatId } = req.query as InitChatProps;
if (!appId) {
return jsonRes(res, {
code: 501,
message: "You don't have an app yet"
});
}
async function handler(req: NextApiRequest, res: NextApiResponse): Promise<InitChatResponse> {
const { appId, chatId } = InitChatQuerySchema.parse(req.query);
try {
// auth app permission
......@@ -99,9 +89,3 @@ async function handler(
}
export default NextAPI(handler);
export const config = {
api: {
responseLimit: '10mb'
}
};
......@@ -2,7 +2,6 @@ import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import type { getResDataQuery } from '@/pages/api/core/chat/getResData';
import type {
InitChatProps,
InitChatResponse,
InitOutLinkChatProps,
InitTeamChatProps
......@@ -24,7 +23,10 @@ import type {
UpdateFavouriteAppParamsType
} from '@fastgpt/global/openapi/core/chat/favourite/api';
import type { ChatFavouriteAppType } from '@fastgpt/global/core/chat/favouriteApp/type';
import type { StopV2ChatParams } from '@fastgpt/global/openapi/core/chat/controler/api';
import type {
InitChatQueryType,
StopV2ChatParams
} from '@fastgpt/global/openapi/core/chat/controler/api';
import type { GetRecentlyUsedAppsResponseType } from '@fastgpt/global/openapi/core/chat/api';
export const getRecentlyUsedApps = () =>
......@@ -33,7 +35,7 @@ export const getRecentlyUsedApps = () =>
/**
* 获取初始化聊天内容
*/
export const getInitChatInfo = (data: InitChatProps) =>
export const getInitChatInfo = (data: InitChatQueryType) =>
GET<InitChatResponse>(`/core/chat/init`, data);
export const getInitOutLinkChatInfo = (data: InitOutLinkChatProps) =>
GET<InitChatResponse>(`/core/chat/outLink/init`, data);
......
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