159 lines
7.4 KiB
JavaScript
159 lines
7.4 KiB
JavaScript
// ============================================================
|
||
// 辩论协调器 — 8 步辩论流程编排
|
||
// 移植自 Counsel AI 的结构化辩论方法论
|
||
// ============================================================
|
||
|
||
const { callLLM } = require('./llm.js');
|
||
const { DEFAULT_PERSONAS } = require('./personas.js');
|
||
const {
|
||
definePrompt, factQuestionPrompt, opinionPrompt,
|
||
dimensionsPrompt, debatePrompt, debateFacilitatorPrompt,
|
||
summaryPrompt, harvestPrompt
|
||
} = require('./prompts.js');
|
||
|
||
// 记录每步的时间和信息
|
||
const stepLog = [];
|
||
function logStep(step, status, detail) {
|
||
stepLog.push({ step, status, detail, time: new Date().toISOString() });
|
||
console.log(`[辩论] Step ${step}: ${status} - ${detail?.substring(0, 80)}`);
|
||
}
|
||
|
||
// 8 步辩论流程
|
||
async function runDebate(input, options = {}) {
|
||
const {
|
||
personas = DEFAULT_PERSONAS,
|
||
userAnswers = {}, // 用户对事实性问题的回答
|
||
model = 'deepseek-chat',
|
||
maxTokens = 2048
|
||
} = options;
|
||
|
||
if (!input || !input.trim()) {
|
||
throw new Error("请输入要辩论的问题");
|
||
}
|
||
|
||
logStep(0, "start", `输入问题: ${input.substring(0, 60)}...`);
|
||
const state = { rawInput: input.trim(), defined: '', answers: '', opinions: [], dimensions: [], debates: [], summary: '', harvest: '' };
|
||
|
||
// === Step 1: 问题精确定义 ===
|
||
logStep(1, "running", "Facilitator 精确定义问题...");
|
||
try {
|
||
const prompt1 = definePrompt(state.rawInput);
|
||
const r1 = await callLLM({ messages: [{ role: 'user', content: prompt1 }], model, maxTokens });
|
||
state.defined = r1.choices?.[0]?.message?.content || prompt1;
|
||
logStep(1, "done", `问题定义: ${state.defined.substring(0, 100)}`);
|
||
} catch(e) {
|
||
logStep(1, "fallback", "LLM 调用失败, 使用原始输入作为问题定义");
|
||
state.defined = state.rawInput;
|
||
}
|
||
|
||
// === Step 2: 事实追问 ===
|
||
logStep(2, "running", `${personas.length} 位幕僚轮流问事实性问题...`);
|
||
try {
|
||
const factResults = [];
|
||
for (const p of personas) {
|
||
const prevQA = factResults.map((r, i) => `Q: ${r.question}\nA: ${r.answer || '(未回答)'}`).join('\n');
|
||
const prompt2 = factQuestionPrompt(state.rawInput, state.defined, p.skill, prevQA);
|
||
const r2 = await callLLM({ messages: [{ role: 'user', content: prompt2 }], model, maxTokens: 1024 });
|
||
const question = r2.choices?.[0]?.message?.content || '';
|
||
if (question && !question.includes('没有问题了') && !question.includes('无需')) {
|
||
const answer = userAnswers[p.id] || userAnswers[p.name] || '(待用户回答)';
|
||
factResults.push({ persona: p.name, question, answer });
|
||
}
|
||
}
|
||
state.answers = factResults.map(r => `**${r.persona}** 问:${r.question}\n答:${r.answer}`).join('\n\n');
|
||
logStep(2, "done", `收集了 ${factResults.length} 个事实性问题`);
|
||
} catch(e) {
|
||
logStep(2, "fallback", `事实追问失败: ${e.message}`);
|
||
state.answers = '(未收集事实信息)';
|
||
}
|
||
|
||
// === Step 3: 表态(12 路并行)===
|
||
logStep(3, "running", `${personas.length} 路并行表态...`);
|
||
try {
|
||
const opinionPromises = personas.map(p =>
|
||
callLLM({ messages: [{ role: 'user', content: opinionPrompt(state.rawInput, state.defined, state.answers, p.skill) }], model, maxTokens: 1024 })
|
||
.then(r => ({ persona: p.name, emoji: p.emoji, opinion: r.choices?.[0]?.message?.content || '(无回应)' }))
|
||
.catch(e => ({ persona: p.name, emoji: p.emoji, opinion: `(调用失败: ${e.message})` }))
|
||
);
|
||
const opinions = await Promise.all(opinionPromises);
|
||
state.opinions = opinions;
|
||
logStep(3, "done", `全部 ${opinions.length} 位幕僚表态完成`);
|
||
} catch(e) {
|
||
logStep(3, "error", `表态失败: ${e.message}`);
|
||
state.opinions = personas.map(p => ({ persona: p.name, emoji: p.emoji, opinion: '(获取失败)' }));
|
||
}
|
||
|
||
// === Step 4: 冲突维度提炼 ===
|
||
logStep(4, "running", "Facilitator 提炼冲突维度...");
|
||
try {
|
||
const opinionsText = state.opinions.map(o => `**${o.emoji} ${o.persona}**:${o.opinion}`).join('\n\n');
|
||
const prompt4 = dimensionsPrompt(opinionsText);
|
||
const r4 = await callLLM({ messages: [{ role: 'user', content: prompt4 }], model, maxTokens });
|
||
const dimText = r4.choices?.[0]?.message?.content || '';
|
||
state.dimensions = dimText.split(/## 维度 \d+/).filter(Boolean).map(d => d.trim()).filter(d => d.length > 0);
|
||
if (state.dimensions.length === 0 && dimText.trim()) {
|
||
state.dimensions = [dimText.trim()];
|
||
}
|
||
logStep(4, "done", `提炼了 ${state.dimensions.length} 个冲突维度`);
|
||
} catch(e) {
|
||
logStep(4, "fallback", `维度提炼失败: ${e.message}`);
|
||
state.dimensions = ['(无法提炼维度)'];
|
||
}
|
||
|
||
// === Step 5: 维度辩论 ===
|
||
logStep(5, "running", `对 ${state.dimensions.length} 个维度进行辩论...`);
|
||
try {
|
||
const debateResults = [];
|
||
for (let i = 0; i < state.dimensions.length; i++) {
|
||
const dim = state.dimensions[i];
|
||
const dimShort = dim.substring(0, 60);
|
||
logStep(5, "sub", `维度 ${i+1}/${state.dimensions.length}: ${dimShort}...`);
|
||
const debatePromises = personas.map(p =>
|
||
callLLM({ messages: [{ role: 'user', content: debatePrompt(state.defined, state.answers, dim, p.skill) }], model, maxTokens: 1024 })
|
||
.then(r => ({ persona: p.name, emoji: p.emoji, stance: r.choices?.[0]?.message?.content || '(无回应)' }))
|
||
.catch(e => ({ persona: p.name, emoji: p.emoji, stance: `(调用失败: ${e.message})` }))
|
||
);
|
||
const stances = await Promise.all(debatePromises);
|
||
const allPositions = stances.map(s => `**${s.emoji} ${s.persona}**:${s.stance}`).join('\n');
|
||
const summaryP = debateFacilitatorPrompt(dim, allPositions);
|
||
const rSum = await callLLM({ messages: [{ role: 'user', content: summaryP }], model, maxTokens: 1024 });
|
||
const dimSummary = rSum.choices?.[0]?.message?.content || '(无总结)';
|
||
debateResults.push({ dimension: dim, stances, summary: dimSummary });
|
||
}
|
||
state.debates = debateResults;
|
||
logStep(5, "done", `全部 ${state.dimensions.length} 个维度辩论完成`);
|
||
} catch(e) {
|
||
logStep(5, "error", `辩论失败: ${e.message}`);
|
||
state.debates = state.dimensions.map(dim => ({ dimension: dim, stances: [], summary: '(辩论失败)' }));
|
||
}
|
||
|
||
// === Step 6: 结构化总结 ===
|
||
logStep(6, "running", "Secretary 生成结构化总结报告...");
|
||
try {
|
||
const debateRecord = state.debates.map(d => `## ${d.dimension.substring(0, 80)}\n${d.summary}`).join('\n\n');
|
||
const prompt6 = summaryPrompt(state.rawInput, state.defined, state.answers, debateRecord);
|
||
const r6 = await callLLM({ messages: [{ role: 'user', content: prompt6 }], model, maxTokens: 4096 });
|
||
state.summary = r6.choices?.[0]?.message?.content || '(生成失败)';
|
||
logStep(6, "done", "结构化总结完成");
|
||
} catch(e) {
|
||
logStep(6, "error", `总结失败: ${e.message}`);
|
||
state.summary = '(总结生成失败)';
|
||
}
|
||
|
||
// === Step 7: 摘果子 ===
|
||
logStep(7, "running", "提取 To-Do 和关键评估...");
|
||
try {
|
||
const prompt7 = harvestPrompt(state.summary);
|
||
const r7 = await callLLM({ messages: [{ role: 'user', content: prompt7 }], model, maxTokens: 2048 });
|
||
state.harvest = r7.choices?.[0]?.message?.content || '(生成失败)';
|
||
logStep(7, "done", "评估和建议提取完成");
|
||
} catch(e) {
|
||
logStep(7, "error", `摘果子失败: ${e.message}`);
|
||
state.harvest = '(评估生成失败)';
|
||
}
|
||
|
||
return { state, stepLog };
|
||
}
|
||
|
||
module.exports = { runDebate, stepLog };
|