mirror of
https://github.com/jeffusion/gitea-ai-assistant.git
synced 2026-03-27 10:05:50 +00:00
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.
339 lines
9.5 KiB
TypeScript
339 lines
9.5 KiB
TypeScript
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,
|
||
};
|
||
}
|
||
}
|