|
@@ -14,6 +14,7 @@ const guard = require('./guard.js');
|
|
|
const prompt = require('./prompt.js');
|
|
const prompt = require('./prompt.js');
|
|
|
const registry = require('./tools/registry.js');
|
|
const registry = require('./tools/registry.js');
|
|
|
const skills = require('./skills.js');
|
|
const skills = require('./skills.js');
|
|
|
|
|
+const metrics = require('./metrics.js');
|
|
|
const provenance = require('./provenance.js');
|
|
const provenance = require('./provenance.js');
|
|
|
const trace = require('./trace.js');
|
|
const trace = require('./trace.js');
|
|
|
const nlQuery = require('../../nl-query.js');
|
|
const nlQuery = require('../../nl-query.js');
|
|
@@ -68,7 +69,9 @@ async function runAgent(o) {
|
|
|
svc, entityName: meta.name || rec.entity, shops, dateRange, today,
|
|
svc, entityName: meta.name || rec.entity, shops, dateRange, today,
|
|
|
toolsText: registry.manifestText(),
|
|
toolsText: registry.manifestText(),
|
|
|
skillsText: skills.manifestText(),
|
|
skillsText: skills.manifestText(),
|
|
|
|
|
+ metricsText: metrics.manifestText(),
|
|
|
pointedSkill: rec.skill ? skills.get(rec.skill) : null,
|
|
pointedSkill: rec.skill ? skills.get(rec.skill) : null,
|
|
|
|
|
+ fewShotsText: o.fewShotsText || '',
|
|
|
}),
|
|
}),
|
|
|
}];
|
|
}];
|
|
|
for (const h of rec.history || []) messages.push(h);
|
|
for (const h of rec.history || []) messages.push(h);
|
|
@@ -88,10 +91,18 @@ async function runAgent(o) {
|
|
|
if (mock) {
|
|
if (mock) {
|
|
|
return { reply: mock.next(rec), usage: null, ms: Date.now() - t0 };
|
|
return { reply: mock.next(rec), usage: null, ms: Date.now() - t0 };
|
|
|
}
|
|
}
|
|
|
|
|
+ // 模型路由:深度分析优先用专用模型(chatModelAgent),缺省回落主对话模型
|
|
|
const out = await llm.chatCompletionDetailed({
|
|
const out = await llm.chatCompletionDetailed({
|
|
|
- apiBase: cfg.chatApiBase, apiKey: cfg.chatApiKey, model: cfg.chatModel,
|
|
|
|
|
|
|
+ apiBase: cfg.chatApiBase, apiKey: cfg.chatApiKey,
|
|
|
|
|
+ model: cfg.chatModelAgent || cfg.chatModel,
|
|
|
messages: msgs, timeoutMs: guard.BUDGET.llmTimeoutMs, temperature: 0.2,
|
|
messages: msgs, timeoutMs: guard.BUDGET.llmTimeoutMs, temperature: 0.2,
|
|
|
|
|
+ jsonMode: !!cfg.chatJsonMode,
|
|
|
});
|
|
});
|
|
|
|
|
+ // 治理:run 级 token 用量累计(成本展示)
|
|
|
|
|
+ if (out.usage) {
|
|
|
|
|
+ rec.usage.tokensIn = (rec.usage.tokensIn || 0) + (out.usage.promptTokens || 0);
|
|
|
|
|
+ rec.usage.tokensOut = (rec.usage.tokensOut || 0) + (out.usage.completionTokens || 0);
|
|
|
|
|
+ }
|
|
|
return { reply: out.content, usage: out.usage, ms: Date.now() - t0 };
|
|
return { reply: out.content, usage: out.usage, ms: Date.now() - t0 };
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -108,6 +119,38 @@ async function runAgent(o) {
|
|
|
};
|
|
};
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ /** 工具执行(主循环与复用打法共用):留痕/verdict 归档/应用与报告链接捕获 */
|
|
|
|
|
+ const executeTool = async (toolName, tool, args) => {
|
|
|
|
|
+ rec.usage.toolCalls += 1;
|
|
|
|
|
+ const step = { type: 'tool', tool: toolName, args, ok: false, ms: 0 };
|
|
|
|
|
+ rec.steps.push(step);
|
|
|
|
|
+ trace.pushEvent(rec, 'tool.start', { tool: toolName });
|
|
|
|
|
+ const t0 = Date.now();
|
|
|
|
|
+ try {
|
|
|
|
|
+ const out = await tool.handler(ctx, args);
|
|
|
|
|
+ step.ok = true;
|
|
|
|
|
+ step.summary = String(out.summary || '').slice(0, 4000);
|
|
|
|
|
+ step.data = out.data || null;
|
|
|
|
|
+ if (out.data && out.data.totalRows != null) step.rows = out.data.totalRows;
|
|
|
|
|
+ step.bytes = JSON.stringify(out.data || null).length;
|
|
|
|
|
+ // 校验类工具的 verdict 归档(quality/identity/cross-layer/freshness/coverage/cloud)
|
|
|
|
|
+ if (out.data && Array.isArray(out.data.verdicts)) {
|
|
|
|
|
+ for (const v of out.data.verdicts) {
|
|
|
|
|
+ rec.verdicts.push({ id: `v${rec.verdicts.length + 1}`, tool: toolName, ...v });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const applied = appliedFrom(toolName, args);
|
|
|
|
|
+ if (applied) lastApplied = applied;
|
|
|
|
|
+ if (toolName === 'build_report' && out.data && out.data.url) lastReportUrl = out.data.url;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ step.error = String(e.message || e).slice(0, 500);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ step.ms = Date.now() - t0;
|
|
|
|
|
+ trace.pushEvent(rec, 'tool.done', { tool: toolName, ok: step.ok, ms: step.ms, error: step.error });
|
|
|
|
|
+ }
|
|
|
|
|
+ return step;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const forceFinal = async (reason) => {
|
|
const forceFinal = async (reason) => {
|
|
|
messages.push({ role: 'user', content: `${reason}:请立即基于以上已有信息输出 final(JSON),不要再调用工具。` });
|
|
messages.push({ role: 'user', content: `${reason}:请立即基于以上已有信息输出 final(JSON),不要再调用工具。` });
|
|
|
try {
|
|
try {
|
|
@@ -118,6 +161,28 @@ async function runAgent(o) {
|
|
|
return { answer: fallbackAnswer(rec, reason), chart: null, appliedFrom: lastApplied, reportUrl: lastReportUrl, thought: '' };
|
|
return { answer: fallbackAnswer(rec, reason), chart: null, appliedFrom: lastApplied, reportUrl: lastReportUrl, thought: '' };
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // 打法复用:命中已验证查询序列 → 直接重跑(缓存的是打法,数据按当前湖重算)→ 单次收敛
|
|
|
|
|
+ if (o.plan && Array.isArray(o.plan.steps) && o.plan.steps.length) {
|
|
|
|
|
+ rec.replayFrom = o.plan.question;
|
|
|
|
|
+ rec.steps.push({ type: 'thought', text: `命中已验证打法「${o.plan.question}」,直接复用其查询序列(数据按当前湖重算)`, ms: 0 });
|
|
|
|
|
+ for (const ps of o.plan.steps) {
|
|
|
|
|
+ if (rec.cancelRequested) throw cancelledError();
|
|
|
|
|
+ const tool = registry.get(ps.tool);
|
|
|
|
|
+ if (!tool) continue;
|
|
|
|
|
+ let args;
|
|
|
|
|
+ try { args = tool.sanitize(ps.args || {}, ctx); } catch { continue; } // 口径变更等导致的参数失配:跳过该步
|
|
|
|
|
+ const step = await executeTool(ps.tool, tool, args);
|
|
|
|
|
+ messages.push({ role: 'user', content: `[工具结果] ${ps.tool}\n${step.summary || step.error || ''}` });
|
|
|
|
|
+ }
|
|
|
|
|
+ messages.push({ role: 'user', content: '以上为按既有打法重跑的最新数据结果,请立即基于其输出 final(JSON),不要再调用工具。' });
|
|
|
|
|
+ try {
|
|
|
|
|
+ const turn = await callModel(messages);
|
|
|
|
|
+ const parsed = llm.parseAgentAction(turn.reply);
|
|
|
|
|
+ if (parsed.type === 'final') return finishWith(parsed);
|
|
|
|
|
+ } catch { /* 收敛失败走兜底 */ }
|
|
|
|
|
+ return { answer: fallbackAnswer(rec, '复用打法收敛失败'), chart: null, appliedFrom: lastApplied, reportUrl: lastReportUrl, thought: '' };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
while (true) {
|
|
while (true) {
|
|
|
if (rec.cancelRequested) throw cancelledError();
|
|
if (rec.cancelRequested) throw cancelledError();
|
|
|
|
|
|
|
@@ -201,37 +266,12 @@ async function runAgent(o) {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- rec.usage.toolCalls += 1;
|
|
|
|
|
- const step = { type: 'tool', tool: parsed.tool, args, ok: false, ms: 0 };
|
|
|
|
|
- rec.steps.push(step);
|
|
|
|
|
- trace.pushEvent(rec, 'tool.start', { tool: parsed.tool });
|
|
|
|
|
- const t0 = Date.now();
|
|
|
|
|
- try {
|
|
|
|
|
- const out = await tool.handler(ctx, args);
|
|
|
|
|
- step.ok = true;
|
|
|
|
|
- step.summary = String(out.summary || '').slice(0, 4000);
|
|
|
|
|
- step.data = out.data || null;
|
|
|
|
|
- if (out.data && out.data.totalRows != null) step.rows = out.data.totalRows;
|
|
|
|
|
- step.bytes = JSON.stringify(out.data || null).length;
|
|
|
|
|
- // 校验类工具的 verdict 归档(quality/identity/cross-layer/freshness/coverage/peer)
|
|
|
|
|
- if (out.data && Array.isArray(out.data.verdicts)) {
|
|
|
|
|
- for (const v of out.data.verdicts) {
|
|
|
|
|
- rec.verdicts.push({ id: `v${rec.verdicts.length + 1}`, tool: parsed.tool, ...v });
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- const applied = appliedFrom(parsed.tool, args);
|
|
|
|
|
- if (applied) lastApplied = applied;
|
|
|
|
|
- if (parsed.tool === 'build_report' && out.data && out.data.url) lastReportUrl = out.data.url;
|
|
|
|
|
- messages.push({ role: 'user', content: `[工具结果] ${parsed.tool}\n${step.summary}` });
|
|
|
|
|
- if (verdict === 'repeat') {
|
|
|
|
|
- messages.push({ role: 'user', content: '提醒:这与上一次调用完全相同。若信息已足够请输出 final;确需重试请更换参数。' });
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- step.error = String(e.message || e).slice(0, 500);
|
|
|
|
|
- messages.push({ role: 'user', content: `[工具失败] ${parsed.tool}:${step.error}。请修正参数重试或换路径。` });
|
|
|
|
|
- } finally {
|
|
|
|
|
- step.ms = Date.now() - t0;
|
|
|
|
|
- trace.pushEvent(rec, 'tool.done', { tool: parsed.tool, ok: step.ok, ms: step.ms, error: step.error });
|
|
|
|
|
|
|
+ const step = await executeTool(parsed.tool, tool, args);
|
|
|
|
|
+ messages.push(step.ok
|
|
|
|
|
+ ? { role: 'user', content: `[工具结果] ${parsed.tool}\n${step.summary}` }
|
|
|
|
|
+ : { role: 'user', content: `[工具失败] ${parsed.tool}:${step.error}。请修正参数重试或换路径。` });
|
|
|
|
|
+ if (step.ok && verdict === 'repeat') {
|
|
|
|
|
+ messages.push({ role: 'user', content: '提醒:这与上一次调用完全相同。若信息已足够请输出 final;确需重试请更换参数。' });
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|