| 350 | ]; |
| 351 | |
| 352 | function assessCompletionOutcome(summary: RequestSummary, payload: RequestPayload, stopReason?: string): CompletionAssessment { |
| 353 | const finalText = [payload.finalResponse, payload.rawResponse] |
| 354 | .find((text): text is string => typeof text === 'string' && text.trim().length > 0) |
| 355 | ?.trim() || ''; |
| 356 | |
| 357 | const issueTags: string[] = []; |
| 358 | const reasonParts: string[] = []; |
| 359 | |
| 360 | const missingToolExecution = summary.hasTools |
| 361 | && summary.toolCallsDetected === 0 |
| 362 | && finalText.length > 0 |
| 363 | && TOOL_UNAVAILABLE_PATTERNS.some(pattern => pattern.test(finalText)); |
| 364 | |
| 365 | if (missingToolExecution) { |
| 366 | issueTags.push('tool_unavailable'); |
| 367 | reasonParts.push('模型声称工具不可用,未执行实际工具调用'); |
| 368 | } |
| 369 | |
| 370 | const truncatedWithoutRecovery = stopReason === 'max_tokens' && summary.continuationCount === 0; |
| 371 | if (truncatedWithoutRecovery) { |
| 372 | issueTags.push('truncated_output'); |
| 373 | reasonParts.push('响应触发 max_tokens 且未自动续写'); |
| 374 | } |
| 375 | |
| 376 | const selfRepairAfterCutoff = summary.hasTools |
| 377 | && finalText.length > 0 |
| 378 | && SELF_REPAIR_AFTER_CUTOFF_PATTERNS.some(pattern => pattern.test(finalText)); |
| 379 | if (selfRepairAfterCutoff) { |
| 380 | issueTags.push('self_repair_after_cutoff'); |
| 381 | reasonParts.push('模型自述上一步输出或写入被截断,当前请求在补救补写'); |
| 382 | } |
| 383 | |
| 384 | if (issueTags.length > 0) { |
| 385 | return { |
| 386 | status: 'degraded', |
| 387 | statusReason: reasonParts.join(';'), |
| 388 | issueTags, |
| 389 | }; |
| 390 | } |
| 391 | |
| 392 | return { status: 'success' }; |
| 393 | } |
| 394 | |
| 395 | function buildCompactOriginalRequest(summary: RequestSummary, payload: RequestPayload): Record<string, unknown> | undefined { |
| 396 | const original = payload.originalRequest && typeof payload.originalRequest === 'object' && !Array.isArray(payload.originalRequest) |