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,
|
getToolsForClaudeAPI,
|
||||||
} from '../tools/coordinator-tools';
|
} from '../tools/coordinator-tools';
|
||||||
import {
|
import {
|
||||||
INTENT_MAX_ANSWER_LENGTH,
|
|
||||||
MAX_FOLLOWUP_LENGTH,
|
MAX_FOLLOWUP_LENGTH,
|
||||||
smartTruncate,
|
smartTruncate,
|
||||||
} from '../schemas/coordinator-response.schema';
|
} from '../schemas/coordinator-response.schema';
|
||||||
|
|
@ -351,48 +350,39 @@ export async function* agentLoop(
|
||||||
.map(b => b.text)
|
.map(b => b.text)
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
// ---- Structured Output 解析:从 JSON 中提取 answer,强制截断,yield ----
|
// ---- Structured Output 解析:从 JSON 中提取 answer + followUp ----
|
||||||
|
// 长度控制由 提示词 + Schema描述 + LLM-Judge 三层负责
|
||||||
|
// 这里只做 JSON 解析 + 安全网(2000字极端情况保护)
|
||||||
if (params.outputConfig && responseText) {
|
if (params.outputConfig && responseText) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(responseText);
|
const parsed = JSON.parse(responseText);
|
||||||
if (parsed.answer) {
|
if (parsed.answer) {
|
||||||
// 按 intent 强制截断 answer
|
const SAFETY_NET = 2000; // 极端情况安全网,正常不会触发
|
||||||
const maxLen = INTENT_MAX_ANSWER_LENGTH[parsed.intent] || 300;
|
let answer = parsed.answer;
|
||||||
const originalLen = parsed.answer.length;
|
if (answer.length > SAFETY_NET) {
|
||||||
const answer = smartTruncate(parsed.answer, maxLen);
|
answer = smartTruncate(answer, SAFETY_NET);
|
||||||
|
logger.warn(
|
||||||
if (originalLen > maxLen) {
|
`[Turn ${currentTurn + 1}] Answer hit safety net: ${parsed.answer.length} → ${answer.length} chars (intent=${parsed.intent})`,
|
||||||
logger.debug(
|
|
||||||
`[Turn ${currentTurn + 1}] Answer truncated: ${originalLen} → ${answer.length} chars (intent=${parsed.intent}, limit=${maxLen})`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
yield { type: 'text', content: answer, timestamp: Date.now() };
|
yield { type: 'text', content: answer, timestamp: Date.now() };
|
||||||
|
|
||||||
// followUp 处理:
|
// followUp:含?→ 跟进问题;否则直接追加(模型可能将内容分到 followUp)
|
||||||
// 1. 含 ?/? → 作为跟进问题输出
|
|
||||||
// 2. answer 未以句末标点结束 → followUp 可能是 answer 的延续部分,追加输出
|
|
||||||
// 3. 其余情况 → 过滤掉(内部策略备注等)
|
|
||||||
if (parsed.followUp) {
|
if (parsed.followUp) {
|
||||||
const isQuestion = /?|\?/.test(parsed.followUp);
|
if (/?|\?/.test(parsed.followUp)) {
|
||||||
const answerEndsClean = /[。!?;!?]$/.test(answer.replace(/\.{3}$/, ''));
|
|
||||||
|
|
||||||
if (isQuestion) {
|
|
||||||
const followUp = smartTruncate(parsed.followUp, MAX_FOLLOWUP_LENGTH);
|
const followUp = smartTruncate(parsed.followUp, MAX_FOLLOWUP_LENGTH);
|
||||||
yield { type: 'text', content: '\n\n' + followUp, timestamp: Date.now() };
|
yield { type: 'text', content: '\n\n' + followUp, timestamp: Date.now() };
|
||||||
} else if (!answerEndsClean) {
|
} else {
|
||||||
// answer 被截断或模型将内容分到 followUp → 追加(不换行)
|
// 非问题的 followUp 直接追加(防止内容丢失)
|
||||||
logger.debug(`[Turn ${currentTurn + 1}] followUp appended as answer continuation`);
|
|
||||||
yield { type: 'text', content: parsed.followUp, timestamp: Date.now() };
|
yield { type: 'text', content: parsed.followUp, timestamp: Date.now() };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// JSON 合法但缺少 answer 字段 → yield 原始文本
|
|
||||||
yield { type: 'text', content: responseText, timestamp: Date.now() };
|
yield { type: 'text', content: responseText, timestamp: Date.now() };
|
||||||
}
|
}
|
||||||
logger.debug(`[Turn ${currentTurn + 1}] Structured output intent: ${parsed.intent}`);
|
logger.debug(`[Turn ${currentTurn + 1}] Structured output intent: ${parsed.intent}`);
|
||||||
} catch {
|
} catch {
|
||||||
// JSON 解析失败 → 回退到原始文本
|
|
||||||
logger.warn(`[Turn ${currentTurn + 1}] Structured output parse failed, falling back to raw text`);
|
logger.warn(`[Turn ${currentTurn + 1}] Structured output parse failed, falling back to raw text`);
|
||||||
yield { type: 'text', content: responseText, timestamp: Date.now() };
|
yield { type: 'text', content: responseText, timestamp: Date.now() };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,21 +23,6 @@ export const CoordinatorResponseSchema = z.object({
|
||||||
|
|
||||||
export type CoordinatorResponse = z.infer<typeof CoordinatorResponseSchema>;
|
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 问题最大长度 */
|
/** followUp 问题最大长度 */
|
||||||
export const MAX_FOLLOWUP_LENGTH = 80;
|
export const MAX_FOLLOWUP_LENGTH = 80;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue