refactor(agents): remove per-intent hard truncation, keep 2000-char safety net
Hard-coded INTENT_MAX_ANSWER_LENGTH limits caused mid-sentence truncation and content loss. Length control now relies on prompt + schema description + LLM-Judge (3 layers). Only a 2000-char safety net remains for extreme edge cases. Also simplified followUp: non-question followUp is now always appended (prevents model content split from silently dropping text). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b55cd4bc1e
commit
db8617dda8
|
|
@ -32,7 +32,6 @@ import {
|
|||
getToolsForClaudeAPI,
|
||||
} from '../tools/coordinator-tools';
|
||||
import {
|
||||
INTENT_MAX_ANSWER_LENGTH,
|
||||
MAX_FOLLOWUP_LENGTH,
|
||||
smartTruncate,
|
||||
} from '../schemas/coordinator-response.schema';
|
||||
|
|
@ -351,48 +350,39 @@ export async function* agentLoop(
|
|||
.map(b => b.text)
|
||||
.join('');
|
||||
|
||||
// ---- Structured Output 解析:从 JSON 中提取 answer,强制截断,yield ----
|
||||
// ---- Structured Output 解析:从 JSON 中提取 answer + followUp ----
|
||||
// 长度控制由 提示词 + Schema描述 + LLM-Judge 三层负责
|
||||
// 这里只做 JSON 解析 + 安全网(2000字极端情况保护)
|
||||
if (params.outputConfig && responseText) {
|
||||
try {
|
||||
const parsed = JSON.parse(responseText);
|
||||
if (parsed.answer) {
|
||||
// 按 intent 强制截断 answer
|
||||
const maxLen = INTENT_MAX_ANSWER_LENGTH[parsed.intent] || 300;
|
||||
const originalLen = parsed.answer.length;
|
||||
const answer = smartTruncate(parsed.answer, maxLen);
|
||||
|
||||
if (originalLen > maxLen) {
|
||||
logger.debug(
|
||||
`[Turn ${currentTurn + 1}] Answer truncated: ${originalLen} → ${answer.length} chars (intent=${parsed.intent}, limit=${maxLen})`,
|
||||
const SAFETY_NET = 2000; // 极端情况安全网,正常不会触发
|
||||
let answer = parsed.answer;
|
||||
if (answer.length > SAFETY_NET) {
|
||||
answer = smartTruncate(answer, SAFETY_NET);
|
||||
logger.warn(
|
||||
`[Turn ${currentTurn + 1}] Answer hit safety net: ${parsed.answer.length} → ${answer.length} chars (intent=${parsed.intent})`,
|
||||
);
|
||||
}
|
||||
|
||||
yield { type: 'text', content: answer, timestamp: Date.now() };
|
||||
|
||||
// followUp 处理:
|
||||
// 1. 含 ?/? → 作为跟进问题输出
|
||||
// 2. answer 未以句末标点结束 → followUp 可能是 answer 的延续部分,追加输出
|
||||
// 3. 其余情况 → 过滤掉(内部策略备注等)
|
||||
// followUp:含?→ 跟进问题;否则直接追加(模型可能将内容分到 followUp)
|
||||
if (parsed.followUp) {
|
||||
const isQuestion = /?|\?/.test(parsed.followUp);
|
||||
const answerEndsClean = /[。!?;!?]$/.test(answer.replace(/\.{3}$/, ''));
|
||||
|
||||
if (isQuestion) {
|
||||
if (/?|\?/.test(parsed.followUp)) {
|
||||
const followUp = smartTruncate(parsed.followUp, MAX_FOLLOWUP_LENGTH);
|
||||
yield { type: 'text', content: '\n\n' + followUp, timestamp: Date.now() };
|
||||
} else if (!answerEndsClean) {
|
||||
// answer 被截断或模型将内容分到 followUp → 追加(不换行)
|
||||
logger.debug(`[Turn ${currentTurn + 1}] followUp appended as answer continuation`);
|
||||
} else {
|
||||
// 非问题的 followUp 直接追加(防止内容丢失)
|
||||
yield { type: 'text', content: parsed.followUp, timestamp: Date.now() };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// JSON 合法但缺少 answer 字段 → yield 原始文本
|
||||
yield { type: 'text', content: responseText, timestamp: Date.now() };
|
||||
}
|
||||
logger.debug(`[Turn ${currentTurn + 1}] Structured output intent: ${parsed.intent}`);
|
||||
} catch {
|
||||
// JSON 解析失败 → 回退到原始文本
|
||||
logger.warn(`[Turn ${currentTurn + 1}] Structured output parse failed, falling back to raw text`);
|
||||
yield { type: 'text', content: responseText, timestamp: Date.now() };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,21 +23,6 @@ export const CoordinatorResponseSchema = z.object({
|
|||
|
||||
export type CoordinatorResponse = z.infer<typeof CoordinatorResponseSchema>;
|
||||
|
||||
// ============================================================
|
||||
// Intent-based Answer Length Limits (中文字符数)
|
||||
// ============================================================
|
||||
|
||||
/** 每种意图对应的最大 answer 长度(字符数)— 程序级硬约束 */
|
||||
export const INTENT_MAX_ANSWER_LENGTH: Record<string, number> = {
|
||||
factual_question: 250, // 1-3句,直接给答案
|
||||
yes_no_question: 150, // 1-2句,结论+理由
|
||||
comparison_question: 300, // 2-3句,推荐+理由
|
||||
assessment_request: 500, // 按需但有上限
|
||||
objection_expression: 350, // 共情+事实+引导(3个组件各需空间)
|
||||
detailed_consultation: 600, // 复杂咨询允许较长
|
||||
casual_chat: 100, // 1句
|
||||
};
|
||||
|
||||
/** followUp 问题最大长度 */
|
||||
export const MAX_FOLLOWUP_LENGTH = 80;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue