fix(bridge): 商品链接兜底与强制检索

This commit is contained in:
wangliang
2026-10-23 13:15:37 +08:00
parent 4d7b89492b
commit 22d96edee6
9 changed files with 414 additions and 3 deletions
+37
View File
@@ -10,6 +10,7 @@ const { createMetrics } = require('./metrics');
const { auditAnswer } = require('./audit');
const { markdownToHtml } = require('./markdown');
const { guardReply } = require('./output-guard');
const { appendMissingProductLinks } = require('./reply-links');
const { quickClassify, detectEmotion, INTENTS } = require('./intent');
const { createContextManager } = require('./context');
const { createReranker } = require('./reranker');
@@ -230,6 +231,26 @@ function createAgent(cfg, deps = {}) {
stats.completionTokens += Number(response.usage.completion_tokens) || 0;
stats.totalTokens += Number(response.usage.total_tokens) || 0;
}
// 兜底(旧 bridge 同款):product_search 意图下首轮 LLM 一个工具都没调时,
// 丢弃它凭记忆编的回答,强制查一次真实商品结果再让它重答,避免幻觉商品/链接进入回复。
if (!(response.toolCalls && response.toolCalls.length) && step === 0 && intent === INTENTS.PRODUCT_SEARCH && !stats.toolCalls.length) {
const searchTool = byName.get('search_products');
if (searchTool) {
const started = Date.now();
try {
const forced = await searchTool.handler({ query: currentText, limit: 5 }, ctx);
stats.toolCalls.push({ name: 'search_products', ms: Date.now() - started, ok: true, forced: true });
stats.toolResults.push(forced);
traceSteps.push({ step_type: 'tool', turn: step, tool_name: 'search_products', duration_ms: Date.now() - started });
messages.push({ role: 'assistant', content: '', tool_calls: [{ id: 'forced-search-products', type: 'function', function: { name: 'search_products', arguments: JSON.stringify({ query: currentText, limit: 5 }) } }] });
messages.push({ role: 'tool', tool_call_id: 'forced-search-products', content: markToolResult('search_products', JSON.stringify(forced).slice(0, 6000)) });
console.log(JSON.stringify({ ts: new Date().toISOString(), agent: 'forced_search_products', conversation: conv.uuid, ok: true }));
continue;
} catch (err) {
console.log(JSON.stringify({ ts: new Date().toISOString(), agent: 'forced_search_products', conversation: conv.uuid, ok: false, error: err.message }));
}
}
}
if (response.toolCalls && response.toolCalls.length) {
messages.push({ role: 'assistant', content: response.content || '', tool_calls: response.toolCalls });
for (const call of response.toolCalls) {
@@ -272,6 +293,22 @@ function createAgent(cfg, deps = {}) {
if (!finalContent) {
finalContent = '抱歉,我这边暂时无法处理,请您稍后再试。';
}
// 确定性链接兜底(旧 bridge buildProductListText 同款):LLM 漏写工具返回的真实商品/类目 URL 时,
// 由代码在回复末尾补齐,保证客户至少拿到真实可点链接(放在护栏前,补的链接同样过白名单校验)。
try {
const backfill = appendMissingProductLinks({
content: finalContent,
toolResults: stats.toolResults,
lang: detectLanguage(currentText),
html: /<(p|br|a|ul|ol|strong|div)\b/i.test(finalContent),
});
if (backfill.appended) {
finalContent = backfill.content;
console.log(JSON.stringify({ ts: new Date().toISOString(), agent: 'link_backfill', conversation: conv.uuid, appended: backfill.appended }));
}
} catch (err) {
console.error(JSON.stringify({ ts: new Date().toISOString(), warn: `link backfill failed: ${err.message}` }));
}
// collect_info 兜底:消息含明显 lead 信息但 LLM 没调 collect_info 时,补抽槽位
if (buyingIntent && leadStore && !(stats.toolCalls || []).some((t) => t.name === 'collect_info')) {
try {