Skip to content

Commit 9d2fcd3

Browse files
authored
Merge pull request #219 from Chancemrli/feat/cloudbase-agent-error-parse
feat: 添加LLM返回错误的解析逻辑
2 parents 162c744 + 5ce575e commit 9d2fcd3

5 files changed

Lines changed: 175 additions & 26 deletions

File tree

cloudrunfunctions/cloudbase-agent/src/chat_main.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export class MainChatService {
101101

102102
async afterStream ({ error, needSave, callMsg, chunks, recordId = '' }) {
103103
if (error) {
104-
console.log('请求大模型错误:', error)
104+
console.log('afterStream_error: 请求大模型失败:', JSON.stringify(error))
105105
}
106106
if (needSave && recordId !== '') {
107107
const newChatEntity = new ChatHistoryEntity()

cloudrunfunctions/cloudbase-agent/src/chat_wx.service.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@ import {
1414
BOT_ROLE_ASSISTANT,
1515
BOT_ROLE_USER,
1616
BOT_TYPE_TEXT,
17+
EXCEED_CONCURRENT_REQUEST_LIMIT,
18+
EXCEED_CONCURRENT_REQUEST_LIMIT_MESSAGE,
19+
EXCEED_TOKEN_QUOTA_LIMIT,
20+
EXCEED_TOKEN_QUOTA_LIMIT_MESSAGE,
1721
MSG_TYPE_TEXT,
1822
MSG_TYPE_VOICE,
23+
REQUEST_LLM_ERROR_MESSAGE,
1924
TRIGGER_SRC_WX_CUSTOM_SERVICE,
2025
TRIGGER_SRC_WX_MINI_APP,
2126
TRIGGER_SRC_WX_SERVICE,
@@ -471,7 +476,18 @@ export class WxChatService {
471476
}
472477
})
473478

474-
replyMsgData.content = result.content
479+
if (result.error) {
480+
// LLM 调用失败,映射错误码到用户友好消息
481+
let errMsg = REQUEST_LLM_ERROR_MESSAGE
482+
if (result.error.code === EXCEED_CONCURRENT_REQUEST_LIMIT) {
483+
errMsg = EXCEED_CONCURRENT_REQUEST_LIMIT_MESSAGE
484+
} else if (result.error.code === EXCEED_TOKEN_QUOTA_LIMIT) {
485+
errMsg = EXCEED_TOKEN_QUOTA_LIMIT_MESSAGE
486+
}
487+
replyMsgData.content = errMsg
488+
} else {
489+
replyMsgData.content = result.content
490+
}
475491
}
476492

477493
console.log('replyMsgData:', replyMsgData)

cloudrunfunctions/cloudbase-agent/src/constant.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,41 @@ export const MSG_TYPE_TEXT = 'text'
3434

3535
// 微信回调语音消息类型
3636
export const MSG_TYPE_VOICE = 'voice'
37+
38+
// ==================== LLM 错误码 ====================
39+
40+
/**
41+
* 触发 HUNYUAN rateLimit
42+
*/
43+
export const HUNYUAN_RATE_LIMT_FAILED_CODE = 'HUNYUAN_RATE_LIMIT_FAILED'
44+
45+
export const HUNYUAN_GENERATE_FAILED = 'HUNYUAN_GENERATE_FAILED'
46+
47+
export const LLM_CONTENT_FILER = 'LLM_CONTENT_FILER'
48+
49+
/**
50+
* 请求聊天大模型失败
51+
*/
52+
export const REQUEST_LLM_ERROR_CODE = 'REQUEST_LLM_ERROR'
53+
54+
export const REQUEST_LLM_ERROR_MESSAGE = '调用大模型失败,请稍后重试'
55+
56+
/**
57+
* LLM TOKEN 超限
58+
*/
59+
export const REQUEST_LLM_TOKEN_COUNT_EXCEED = 'REQUEST_LLM_TOKEN_COUNT_EXCEED'
60+
61+
/**
62+
* 大模型请求超限
63+
*/
64+
export const EXCEED_CONCURRENT_REQUEST_LIMIT = 'EXCEED_CONCURRENT_REQUEST_LIMIT'
65+
66+
export const EXCEED_CONCURRENT_REQUEST_LIMIT_MESSAGE = '请求大模型并发超限,请稍后重试'
67+
68+
/**
69+
* 大模型 token 额度超限
70+
*/
71+
export const EXCEED_TOKEN_QUOTA_LIMIT = 'EXCEED_TOKEN_QUOTA_LIMIT'
72+
73+
export const EXCEED_TOKEN_QUOTA_LIMIT_MESSAGE =
74+
'大模型 Token 已耗尽,请通知开发者前往云开发平台进行处理。https://tcb.cloud.tencent.com/dev#/ai'

cloudrunfunctions/cloudbase-agent/src/llm.ts

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import { createDeepSeek } from '@ai-sdk/deepseek'
22
import { createOpenAI } from '@ai-sdk/openai'
3+
import { APICallError } from 'ai'
34
import * as ai from 'ai'
45
import OpenAI from 'openai'
56

67
import { BotContext } from './bot_context'
8+
import {
9+
EXCEED_TOKEN_QUOTA_LIMIT,
10+
EXCEED_TOKEN_QUOTA_LIMIT_MESSAGE,
11+
REQUEST_LLM_ERROR_CODE,
12+
REQUEST_LLM_ERROR_MESSAGE
13+
} from './constant'
714
import { McpClient } from './mcp'
815
import { getAccessToken } from './tcb'
16+
import { extractWithLodash, safeJsonParse } from './utils'
917

1018
const DEEPSEEK_PREFIX = 'deepseek'
1119

@@ -28,8 +36,9 @@ export interface IMsgResult {
2836
content: string;
2937
finish_reason?: string;
3038
error?: {
31-
name: string;
39+
name?: string;
3240
message: string;
41+
code?: string;
3342
};
3443
tool_call?: string;
3544
usage: object;
@@ -173,19 +182,31 @@ export class LLMCommunicator {
173182
messages: ChatCompletionMessage[],
174183
cb: (streamPart: ai.TextStreamPart<ai.ToolSet>) => void
175184
) {
176-
const { fullStream } = ai.streamText({
177-
model: this.model,
178-
tools: await this.mcpClient?.tools(),
179-
maxSteps: 10,
180-
messages: this.tarnsMessage([...messages]),
181-
abortSignal: this.controller.signal,
182-
onFinish: () => {
183-
this.mcpClient?.close()
185+
try {
186+
const { fullStream } = ai.streamText({
187+
model: this.model,
188+
tools: await this.mcpClient?.tools(),
189+
maxSteps: 10,
190+
messages: this.tarnsMessage([...messages]),
191+
abortSignal: this.controller.signal,
192+
onFinish: () => {
193+
this.mcpClient?.close()
194+
}
195+
})
196+
197+
for await (const streamPart of fullStream) {
198+
cb(streamPart)
184199
}
185-
})
200+
} catch (e) {
201+
cb?.({
202+
type: 'error',
203+
content: '',
204+
step: 'error',
205+
error: e,
206+
finishReason: 'error'
207+
})
186208

187-
for await (const streamPart of fullStream) {
188-
cb(streamPart)
209+
console.log('streamText_error:', JSON.stringify({ error: e, msg: e.message }))
189210
}
190211
}
191212

@@ -296,15 +317,20 @@ export class LLMCommunicator {
296317
)
297318
} else if (streamPart.type === 'error') {
298319
// 对话异常
320+
const gwLLMError = this.handlerAICallError(
321+
(streamPart as any).error?.lastError || (streamPart as any).error
322+
)
299323
result = {
300324
...result,
301325
finish_reason: 'error',
302-
error: {
303-
name: 'LLMError',
304-
message: streamPart.error as string
305-
}
326+
error: gwLLMError
306327
}
307-
error = streamPart.error
328+
error = (streamPart as any).error
329+
330+
console.log('stream_error:', JSON.stringify({
331+
msg: (streamPart as any).error,
332+
errMsg: (streamPart as any).error?.message
333+
}))
308334

309335
callMsg.push(result)
310336
this.botContext.bot.sseSender.send(
@@ -316,6 +342,9 @@ export class LLMCommunicator {
316342
}
317343
)
318344
} catch (error) {
345+
const gwLLMError = this.handlerAICallError(
346+
(error as any)?.lastError || error
347+
)
319348
let result: IMsgResult = {
320349
type: 'error',
321350
created: Date.now(),
@@ -327,12 +356,10 @@ export class LLMCommunicator {
327356
result = {
328357
...result,
329358
finish_reason: 'error',
330-
error: {
331-
name: 'LLMError',
332-
message: error as string
333-
}
359+
error: gwLLMError
334360
}
335-
// error = streamPart.error
361+
362+
console.log('stream_error:', JSON.stringify({ error, msg: (error as any)?.message }))
336363

337364
callMsg.push(result)
338365
this.botContext.bot.sseSender.send(`data: ${JSON.stringify(result)}\n\n`)
@@ -369,8 +396,30 @@ export class LLMCommunicator {
369396
const generateTextRes = await ai.generateText(data)
370397
return cb(generateTextRes)
371398
} catch (error) {
372-
console.log(error)
373-
return {}
399+
console.log('generateText_error:', JSON.stringify({ error, msg: (error as any)?.message }))
400+
const gwLLMError = this.handlerAICallError(
401+
(error as any)?.lastError || error
402+
)
403+
return { error: gwLLMError }
404+
}
405+
}
406+
407+
/**
408+
* 解析 LLM 调用错误,转换为统一的内部错误码和友好消息
409+
*/
410+
handlerAICallError (callError: APICallError) {
411+
const responseBody = safeJsonParse(callError?.responseBody)
412+
const result = extractWithLodash(responseBody, ['message', 'code'])
413+
const code = result?.code?.includes(EXCEED_TOKEN_QUOTA_LIMIT)
414+
? EXCEED_TOKEN_QUOTA_LIMIT
415+
: result?.code?.[0] || REQUEST_LLM_ERROR_CODE
416+
let message = result?.message?.[0] || REQUEST_LLM_ERROR_MESSAGE
417+
if (code === EXCEED_TOKEN_QUOTA_LIMIT) {
418+
message = EXCEED_TOKEN_QUOTA_LIMIT_MESSAGE
419+
}
420+
return {
421+
code,
422+
message
374423
}
375424
}
376425
}

cloudrunfunctions/cloudbase-agent/src/utils.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as crypto from 'crypto'
22
import { customAlphabet } from 'nanoid'
3+
import lodash from 'lodash'
34

45
export function genRandomStr (length: number): string {
56
return crypto
@@ -23,3 +24,48 @@ export function safeJsonParse (jsonString: string, defaultValue = null) {
2324
export function randomId (len = 16) {
2425
return customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', len)()
2526
}
27+
28+
// 解析嵌套对象,提取指定key的value
29+
export function extractWithLodash (obj: any, keys: string[], depth: number = 5): any {
30+
if (depth <= 0) {
31+
return {}
32+
}
33+
34+
return lodash.reduce(
35+
obj,
36+
(result: any, value: any, key: string) => {
37+
// 处理直接匹配的键
38+
if (keys.includes(key) && !(lodash.isObject(value) || lodash.isArray(value))) {
39+
result[key] = result[key] || []
40+
result[key].push(value)
41+
}
42+
43+
// 处理嵌套对象或数组
44+
processNestedValue(result, value, keys, depth)
45+
46+
return result
47+
},
48+
{}
49+
)
50+
}
51+
52+
// 处理嵌套对象或数组的递归逻辑
53+
function processNestedValue (result: any, value: any, keys: string[], depth: number) {
54+
if (lodash.isObject(value) && !lodash.isArray(value)) {
55+
const nested = extractWithLodash(value, keys, depth - 1)
56+
lodash.forEach(nested, (values, nestedKey) => {
57+
result[nestedKey] = (result[nestedKey] || []).concat(values)
58+
})
59+
}
60+
61+
if (lodash.isArray(value)) {
62+
value.forEach((item) => {
63+
if (lodash.isObject(item)) {
64+
const nested = extractWithLodash(item, keys, depth - 1)
65+
lodash.forEach(nested, (values, nestedKey) => {
66+
result[nestedKey] = (result[nestedKey] || []).concat(values)
67+
})
68+
}
69+
})
70+
}
71+
}

0 commit comments

Comments
 (0)