Files
archived-gitea-ai-assistant/src/review/agents/debate-orchestrator.ts
jeffusion 1c0c9afd17 feat(review): remove legacy mode and harden agent/codex pipeline
Drop legacy runtime paths and role assignments across backend/frontend, and add upgrade-safe DB migration for existing installs. This aligns config, docs, tests, and UI to the agent-first architecture with codex as the only alternate engine.
2026-03-24 12:30:13 +08:00

339 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import config from '../../config';
import type { LLMGateway } from '../../llm/gateway';
import type { LLMMessage } from '../../llm/types';
import { withCoreGlobalPrompt } from '../../utils/global-prompt';
import { logger } from '../../utils/logger';
import { Finding, FindingSeverity } from '../types';
import { SpecialistAgent } from './specialist-agent';
interface AgentOpinion {
agentName: string;
confidence: number;
severity: FindingSeverity;
reasoning: string;
isValid: boolean;
}
export class DebateOrchestrator {
private gateway: LLMGateway;
constructor(gateway: LLMGateway) {
this.gateway = gateway;
}
async conductDebate(
finding: Omit<Finding, 'id' | 'runId' | 'published'>,
agents: SpecialistAgent[],
maxRounds = 2
): Promise<Omit<Finding, 'id' | 'runId' | 'published'>> {
if (agents.length < 2) {
logger.debug('Debate需要至少2个agents跳过');
return finding;
}
logger.info('启动Debate', {
finding: finding.title,
agentsCount: agents.length,
maxRounds,
});
const opinions = new Map<string, AgentOpinion>();
// 收集初始意见
for (const agent of agents) {
const opinion = await this.getAgentOpinion(agent, finding);
opinions.set((agent as any).agentName, opinion);
}
// 辩论轮次
for (let round = 0; round < maxRounds; round++) {
logger.debug(`Debate Round ${round + 1}/${maxRounds}`, {
finding: finding.title,
});
for (const agent of agents) {
const agentName = (agent as any).agentName;
const otherOpinions = Array.from(opinions.entries()).filter(([name]) => name !== agentName);
const revisedOpinion = await this.reviseOpinion(agent, finding, otherOpinions, opinions);
opinions.set(agentName, revisedOpinion);
}
// 检查是否已达成共识
if (this.hasConsensus(opinions)) {
logger.info(`Debate在第${round + 1}轮达成共识`, {
finding: finding.title,
});
break;
}
}
// 形成共识
return this.formConsensus(finding, opinions);
}
private async getAgentOpinion(
agent: SpecialistAgent,
finding: Omit<Finding, 'id' | 'runId' | 'published'>
): Promise<AgentOpinion> {
const agentName = (agent as any).agentName;
const prompt = `你是${agentName}。评估以下代码问题的严重性、置信度和有效性。
问题:
- Title: ${finding.title}
- Detail: ${finding.detail}
- Evidence: ${finding.evidence}
- Current Severity: ${finding.severity}
- Current Confidence: ${finding.confidence}
从你的专业角度判断:
1. 这个问题是否真实存在(不是误报)?
2. 严重性评估是否准确?
3. 你的置信度是多少?
4. 你的判断理由?
返回JSON
{
"is_valid": true/false,
"confidence": 0.0-1.0,
"severity": "high" | "medium" | "low",
"reasoning": "你的判断理由(详细说明)"
}`;
try {
const messages: LLMMessage[] = [
{
role: 'system',
content: withCoreGlobalPrompt(
`你是${agentName},从你的专业角度独立评估代码问题。`,
config.review.globalPrompt
),
},
{ role: 'user', content: prompt },
];
const response = await this.gateway.chatForRole('specialist', {
messages,
temperature: 0.2,
responseFormat: 'json',
});
const content = response.content;
if (!content) {
throw new Error('Agent opinion返回空');
}
const parsed = JSON.parse(content);
return {
agentName,
// 使用 ?? 而非 ||保留有效的0置信度完全不确定/强烈拒绝)
confidence: parsed.confidence ?? 0.5,
severity: parsed.severity || 'medium',
reasoning: parsed.reasoning || '',
isValid: parsed.is_valid ?? true,
};
} catch (error) {
logger.error(`获取${agentName}意见失败`, {
error: error instanceof Error ? error.message : String(error),
});
return {
agentName,
confidence: finding.confidence,
severity: finding.severity,
reasoning: '获取意见失败,使用默认值',
isValid: true,
};
}
}
private async reviseOpinion(
agent: SpecialistAgent,
finding: Omit<Finding, 'id' | 'runId' | 'published'>,
otherOpinions: [string, AgentOpinion][],
opinions: Map<string, AgentOpinion>
): Promise<AgentOpinion> {
const agentName = (agent as any).agentName;
const prompt = `你是${agentName}。重新评估以下问题,考虑其他专家的意见。
问题:
- Title: ${finding.title}
- Evidence: ${finding.evidence}
其他专家意见:
${otherOpinions
.map(
([name, op]) =>
`- ${name}: ${op.isValid ? '有效' : '误报'}, ${op.severity} (置信度 ${op.confidence.toFixed(2)})\n 理由: ${
op.reasoning
}`
)
.join('\n')}
基于同行的意见,你是否改变观点?
返回JSON
{
"is_valid": true/false,
"confidence": 0.0-1.0,
"severity": "high" | "medium" | "low",
"reasoning": "修正后的理由或坚持原判断的原因"
}`;
try {
const messages: LLMMessage[] = [
{
role: 'system',
content: withCoreGlobalPrompt(
`你是${agentName},根据同行意见重新评估,但也要坚持你的专业判断。`,
config.review.globalPrompt
),
},
{ role: 'user', content: prompt },
];
const response = await this.gateway.chatForRole('specialist', {
messages,
temperature: 0.3, // 允许一定灵活性
responseFormat: 'json',
});
const content = response.content;
if (!content) {
throw new Error('Revised opinion返回空');
}
const parsed = JSON.parse(content);
return {
agentName,
// 使用 ?? 而非 ||保留有效的0置信度完全不确定/强烈拒绝)
confidence: parsed.confidence ?? 0.5,
severity: parsed.severity || 'medium',
reasoning: parsed.reasoning || '',
isValid: parsed.is_valid ?? true,
};
} catch (error) {
logger.error(`${agentName}修订意见失败`, {
error: error instanceof Error ? error.message : String(error),
});
// 返回当前意见从opinions Map中获取
const currentOpinion = opinions.get(agentName);
return (
currentOpinion || {
agentName,
confidence: 0.5,
severity: 'medium',
reasoning: '修订失败',
isValid: true,
}
);
}
}
private hasConsensus(opinions: Map<string, AgentOpinion>): boolean {
const votes = Array.from(opinions.values());
if (votes.length === 0) return true;
// 检查有效性共识至少80%同意)
const validCount = votes.filter((v) => v.isValid).length;
const validRatio = validCount / votes.length;
if (validRatio >= 0.8 || validRatio <= 0.2) {
return true; // 大多数同意有效或无效
}
// 检查严重性共识
const severityCounts: Record<FindingSeverity, number> = {
high: 0,
medium: 0,
low: 0,
};
votes.forEach((v) => {
severityCounts[v.severity]++;
});
const maxCount = Math.max(...Object.values(severityCounts));
const consensusRatio = maxCount / votes.length;
return consensusRatio >= 0.7; // 70%同意同一严重性
}
private formConsensus(
finding: Omit<Finding, 'id' | 'runId' | 'published'>,
opinions: Map<string, AgentOpinion>
): Omit<Finding, 'id' | 'runId' | 'published'> {
const votes = Array.from(opinions.values());
if (votes.length === 0) {
return finding;
}
// 判断有效性(投票)
const validCount = votes.filter((v) => v.isValid).length;
const isValid = validCount > votes.length / 2;
// 如果被判定为无效,降低置信度
if (!isValid) {
logger.info('Debate判定为无效finding', {
finding: finding.title,
validVotes: validCount,
totalVotes: votes.length,
});
return {
...finding,
confidence: Math.min(finding.confidence, 0.4),
detail: `${finding.detail}\n\n**Debate结果**: 多数专家认为此问题可能是误报(${validCount}/${votes.length}认为有效)`,
};
}
// 计算平均置信度仅计算认为有效的votes
const validVotes = votes.filter((v) => v.isValid);
const avgConfidence = validVotes.reduce((sum, v) => sum + v.confidence, 0) / validVotes.length;
// 严重性投票(加权)
const severityVotes: Record<FindingSeverity, number> = {
high: 0,
medium: 0,
low: 0,
};
validVotes.forEach((vote) => {
severityVotes[vote.severity] += vote.confidence;
});
const agreedSeverity =
(Object.entries(severityVotes).sort((a, b) => b[1] - a[1])[0][0] as FindingSeverity) ||
finding.severity;
// 综合推理
const synthesizedDetail = `${finding.detail}\n\n**专家Debate意见汇总**\n${validVotes
.map(
(v) => `- ${v.agentName} (${v.severity}, 置信度${v.confidence.toFixed(2)}): ${v.reasoning}`
)
.join('\n')}`;
logger.info('Debate达成共识', {
finding: finding.title,
originalSeverity: finding.severity,
agreedSeverity,
originalConfidence: finding.confidence,
avgConfidence,
validVotes: validVotes.length,
});
return {
...finding,
confidence: avgConfidence,
severity: agreedSeverity,
detail: synthesizedDetail,
};
}
}