MeshProof: 두 모델이 합의할 때까지 수렴하는 QA 시스템
consensus-verifiers로 만드는 신뢰형 LLM 파이프라인
(2/3) 검증기와 오케스트레이션 엔진 deep dive — 핵심 구현 해부하기
🎯 이번 편에서 다룰 내용
1편에서 MeshProof의 전체 개념을 살펴봤다면, 이번에는 실제 코드 레벨에서 어떻게 구현되는지 깊이 파헤쳐보겠습니다. 특히 각 검증기의 내부 알고리즘과 오케스트레이션 엔진의 핵심 로직을 중심으로 설명합니다.
🧮 MathSumVerifier: 수학 문제의 정교한 검증
수학 문제는 명확한 정답이 있기 때문에 가장 확실한 합의가 가능한 영역입니다. MathSumVerifier는 다양한 표현 방식을 정규화하여 동치성을 판단합니다.
문제 감지와 타겟 추출
export function MathSumVerifier(): Verifier {
// 수학 문제 패턴 정의
const PATTERNS = [
/더해서\s*(-?\d+)\s*가 나오/i, // "더해서 5가 나오는"
/합이\s*(-?\d+)\s*(?:이|가)?/i, // "합이 10이 되는"
/sum(?:s)?\s*(?:to|=)\s*(-?\d+)/i // "sum to 15"
];
function getTarget(question: string): number | null {
for (const regex of PATTERNS) {
const match = question.match(regex);
if (match) return Number(match[1]);
}
return null;
}
return {
name: "math.sum",
supports: (question: string): boolean => getTarget(question) !== null,
verify: (question: string, candidate: Answer): VerifyResult => {
// 구현 상세...
}
};
}
정수쌍 추출의 복잡성
실제로 AI가 정수쌍을 표현하는 방식은 매우 다양합니다:
function extractPairs(text: string): Array<[number, number]> {
const pairs: Array<[number, number]> = [];
// 패턴 1: (x,y) 튜플 형식
for (const m of text.matchAll(/\((-?\d+)\s*[,;]\s*(-?\d+)\)/g)) {
pairs.push([Number(m[1]), Number(m[2])]);
}
// 패턴 2: "x + y" 덧셈 표현
for (const m of text.matchAll(/(-?\d+)\s*\+\s*(-?\d+)/g)) {
pairs.push([Number(m[1]), Number(m[2])]);
}
// 패턴 3: "x와 y", "x, y" (단, 괄호 안은 제외)
for (const m of text.matchAll(/(?<!\()(-?\d+)\s*(?:와|,)\s*(-?\d+)(?!\))/g)) {
pairs.push([Number(m[1]), Number(m[2])]);
}
return pairs;
}
정규화와 동치 판단
가장 핵심적인 부분은 서로 다른 표현을 동일한 형태로 정규화하는 것입니다:
// orchestrator.run.ts에서 사용되는 정규화 함수
function canonicalizePairs(text: string): string {
const pairs = extractPairs(text);
// 각 쌍을 (min, max) 형태로 정규화
const normalized = pairs.map(([a, b]) => [Math.min(a, b), Math.max(a, b)] as [number, number]);
// 중복 제거
const unique = Array.from(new Set(normalized.map(([a,b]) => `${a},${b}`)))
.map(s => s.split(",").map(Number) as [number, number]);
// 정렬하여 순서 무관하게 만들기
unique.sort((p, q) => (p[0] - q[0]) || (p[1] - q[1]));
return JSON.stringify(unique);
}
// 실제 사용 예시
const gptAnswer = "(1,4), (2,3)";
const claudeAnswer = "(2,3), (4,1)";
console.log(canonicalizePairs(gptAnswer)); // "[[1,4],[2,3]]"
console.log(canonicalizePairs(claudeAnswer)); // "[[1,4],[2,3]]"
// → 동일! 합의 달성
검증 로직 완전 구현
verify: (question: string, candidate: Answer): VerifyResult => {
const target = getTarget(question);
if (target === null) return { ok: true, score: 1 };
const fullText = `${candidate.answer} ${candidate.reasoning ?? ""}`;
const pairs = extractPairs(fullText);
if (pairs.length === 0) {
return {
ok: false,
score: 0.2,
notes: ["정수 쌍을 추출하지 못함"]
};
}
// 모든 쌍이 올바른 합을 갖는지 검증
const incorrect = pairs.filter(([a, b]) => a + b !== target);
if (incorrect.length > 0) {
return {
ok: false,
score: 0.2,
notes: incorrect.map(([a, b]) => `합 불일치: ${a}+${b} ≠ ${target}`)
};
}
return { ok: true, score: 1 };
}
🔗 FactCitationVerifier: 출처 검증의 정교함
사실 확인이 중요한 질문에서는 단순히 링크만 있으면 되는 것이 아닙니다. 링크의 품질, 접근성, 날짜 표기 등을 종합적으로 평가해야 합니다.
설정 가능한 검증 옵션
export type FactCitationOptions = {
minLinks?: number; // 최소 링크 수 (기본 1)
requireDeepLinks?: boolean; // 루트 도메인만 링크면 감점
minDeepLinks?: number; // 깊은 링크 최소 개수
requireDatesInReasoning?: boolean; // YYYY-MM-DD 날짜 필수
weights?: DomainWeights; // 도메인별 가중치
};
export type DomainWeights = {
whitelistBoost?: Record<string, number>; // 신뢰할 수 있는 도메인 가산점
blacklistPenalty?: Record<string, number>; // 의심스러운 도메인 감점
};
URL 추출과 정제
function extractUrls(text: string): string[] {
const urlRegex = /\bhttps?:\/\/[^\s)>\]"'`]+/gi;
const evidence = new Set<string>();
// URL 추출 후 후행 구두점 제거
for (const match of text.matchAll(urlRegex)) {
const cleanUrl = match[0].replace(/[),.]+$/g, "");
evidence.add(cleanUrl);
}
return Array.from(evidence);
}
// 깊은 링크 판단
function isDeepLink(url: string): boolean {
try {
const { pathname } = new URL(url);
return pathname !== "/" && pathname.length > 1;
} catch {
return false;
}
}
도메인 가중치 시스템
실제 운영에서는 도메인의 신뢰도에 따라 점수를 차등 부여합니다:
// orchestrator.run.ts에서 실제 사용 중인 가중치
const registry = new VerifierRegistry()
.use(FactCitationVerifier({
minLinks: 2,
requireDeepLinks: true,
minDeepLinks: 1,
requireDatesInReasoning: true,
weights: {
whitelistBoost: {
"stat.kita.net": 0.25, // 무역통계 사이트
"unipass.customs.go.kr": 0.25, // 관세청 통합포털
"motie.go.kr": 0.25, // 산업통상자원부
"kostat.go.kr": 0.25, // 통계청
"bok.or.kr": 0.2, // 한국은행
"kita.net": 0.15, // 무역협회
},
blacklistPenalty: {
"medium.com": 0.2, // 개인 블로그
"blogspot.com": 0.2, // 개인 블로그
"tistory.com": 0.15, // 개인 블로그
"reddit.com": 0.15, // 커뮤니티
}
}
}));
복합적 점수 계산
FactCitationVerifier의 점수는 여러 요소를 종합합니다:
function calculateScore(urls: string[], options: FactCitationOptions): number {
// 1. 기본 통과 여부 (0.6 가중치)
const basePass = urls.length >= (options.minLinks || 1) ? 1 : 0;
// 2. 깊은 링크 비율 (0.2 가중치)
const deepCount = urls.filter(isDeepLink).length;
const deepRatio = urls.length ? (deepCount / urls.length) : 0;
// 3. 도메인 가중치 (0.2 가중치)
const domainWeight = calculateDomainScore(urls, options.weights);
const finalScore = 0.6 * basePass + 0.2 * deepRatio + 0.2 * domainWeight;
return Math.max(0, Math.min(1, finalScore));
}
function calculateDomainScore(urls: string[], weights?: DomainWeights): number {
if (!weights) return 0.5; // 중립
const hostnames = urls.map(url => {
try { return new URL(url).hostname.toLowerCase(); }
catch { return ""; }
}).filter(Boolean);
let boost = 0;
for (const hostname of new Set(hostnames)) {
if (weights.whitelistBoost?.[hostname]) {
boost += weights.whitelistBoost[hostname];
}
if (weights.blacklistPenalty?.[hostname]) {
boost -= weights.blacklistPenalty[hostname];
}
}
// -0.5 ~ +0.5로 클램프 후 0~1로 변환
boost = Math.max(-0.5, Math.min(0.5, boost));
return 0.5 + boost;
}
날짜 표기 검증
const dateRegex = /\b(20\d{2}|19\d{2})[-/.](0[1-9]|1[0-2])([-/.](0[1-9]|[12]\d|3[01]))\b/g;
function hasDateInReasoning(text: string): boolean {
return dateRegex.test(text);
}
// 실제 검증에서의 사용
if (requireDatesInReasoning && !hasDateInReasoning(reasoning)) {
return {
ok: false,
score: Math.min(score, 0.6), // 날짜 없으면 최대 0.6점
notes: [...notes, "reasoning에 자료 기준일(YYYY-MM-DD) 표기 필요"]
};
}
🤖 Judge System: AI 기반 동치성 판정
수학 문제가 아닌 일반적인 질문에서는 두 답변이 본질적으로 같은 내용인지, 아니면 어느 쪽이 더 나은지 판단하기 위해 별도의 Judge AI를 활용합니다.
Judge 시스템 아키텍처
type Verdict = {
relation: "equivalent" | "different"; // 동치 여부
winner: "A" | "B" | "tie"; // 승자 (different일 때)
rationale: string; // 판단 근거
};
export async function judgeOpenAI(
question: string,
answerA: string,
answerB: string,
opts?: { apiKey?: string; model?: string }
): Promise<Verdict> {
const apiKey = opts?.apiKey ?? process.env.OPENAI_API_KEY!;
const model = opts?.model ?? process.env.OPENAI_JUDGE_MODEL ?? "gpt-5";
const client = new OpenAI({ apiKey });
// 구현 상세...
}
엄격한 프롬프트 엔지니어링
Judge의 신뢰성을 높이기 위해서는 명확하고 일관된 지시가 중요합니다:
const systemPrompt = `You are a strict judge. Output a JSON object with keys:
relation: "equivalent"|"different"
winner: "A"|"B"|"tie"
rationale: short string`;
const userPrompt = `Question:
${question}
Answer A:
${answerA}
Answer B:
${answerB}
Decide semantic relation and pick a better answer if different. Return ONLY JSON.`;
안전한 JSON 파싱
AI의 응답이 항상 완벽한 JSON은 아니므로 fallback 로직이 필요합니다:
// GPT-5는 temperature 미지원 → 조건부 파라미터 설정
const isGpt5 = /gpt-5/i.test(model);
const completionParams: any = {
model,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt }
],
response_format: { type: "json_object" }
};
if (!isGpt5) {
completionParams.temperature = 0.2; // 일관성을 위해 낮은 temperature
}
const response = await client.chat.completions.create(completionParams);
const rawContent = response.choices?.[0]?.message?.content ?? "{}";
// 안전한 파싱과 fallback
try {
const verdict = JSON.parse(rawContent) as Verdict;
if (verdict && verdict.relation && verdict.winner) {
return verdict;
}
} catch (error) {
console.warn("Judge JSON parsing failed:", error);
}
// fallback: 보수적 판정
return {
relation: "different",
winner: "tie",
rationale: "fallback due to parsing error"
};
🔄 Orchestration Engine: 복잡한 조율의 핵심
오케스트레이션 엔진은 MeshProof의 뇌 역할을 합니다. 병렬 호출, 검증, 재시도, 합의 판단까지의 전체 과정을 관리합니다.
병렬 모델 호출과 에러 처리
export async function orchestrate(
question: string,
debug = false,
keys?: { openai?: string; anthropic?: string },
opts?: { useOpenAI?: boolean; useClaude?: boolean }
): Promise<FinalOut> {
let gpt: Answer | undefined;
let claude: Answer | undefined;
let gptError: any, claudeError: any;
// 병렬로 두 모델 호출 (실패해도 중단되지 않음)
const [gptResult, claudeResult] = await Promise.allSettled([
opts?.useOpenAI ? askOpenAI(question, { apiKey: keys?.openai }) : null,
opts?.useClaude ? askAnthropic(question, { apiKey: keys?.anthropic }) : null
]);
// 결과 처리
if (gptResult?.status === 'fulfilled' && gptResult.value) {
gpt = gptResult.value;
} else if (gptResult?.status === 'rejected') {
gptError = gptResult.reason;
}
if (claudeResult?.status === 'fulfilled' && claudeResult.value) {
claude = claudeResult.value;
} else if (claudeResult?.status === 'rejected') {
claudeError = claudeResult.reason;
}
// 둘 다 실패하면 에러
if (!gpt && !claude) {
const gptMsg = gptError?.message || "OpenAI unavailable";
const claudeMsg = claudeError?.message || "Anthropic unavailable";
throw new Error(`Both providers failed. openai=${gptMsg} | anthropic=${claudeMsg}`);
}
}
검증과 재시도 메커니즘
출처가 부족한 답변에 대해서는 자동으로 재시도를 수행합니다:
// 재시도 필요성 판단
function needsCitationsRetry(verification?: any, answer?: Answer): boolean {
if (!verification || !answer) return false;
const citationDetail = verification.details?.find((d: any) => d.category === "fact.citation");
if (!citationDetail) return false;
const urls: string[] = citationDetail.evidence ?? [];
const deepLinks = urls.filter(url => {
try {
const pathname = new URL(url).pathname;
return pathname && pathname !== "/";
} catch {
return false;
}
});
const hasDate = /\b(20\d{2}|19\d{2})[-/.](0[1-9]|1[0-2])([-/.](0[1-9]|[12]\d|3[01]))\b/
.test(`${answer.reasoning ?? ""} ${answer.answer ?? ""}`);
return (!citationDetail.ok) ||
(urls.length < 2) ||
(deepLinks.length < 1) ||
!hasDate;
}
// 재시도용 프롬프트 생성
function buildRetrySuffix(): string {
return "\n\n[중요 지시]\n" +
"- reasoning에 최신 출처를 **최소 2개** 포함하세요.\n" +
"- **홈페이지 루트 링크 금지**, 통계표/보도자료 등 **상세 페이지(Deep link)** URL을 사용하세요.\n" +
"- 각 출처 옆에 **발표/게시 날짜(YYYY-MM-DD)**를 표기하세요.\n" +
"- 최종 출력은 JSON 형식 유지: { answer, reasoning, confidence }";
}
// 실제 재시도 실행
if (gpt && needsCitationsRetry(gptVerification, gpt)) {
const retryQuestion = question + buildRetrySuffix();
try {
gpt = await askOpenAI(retryQuestion, { apiKey: keys?.openai });
gptVerification = await registry.verifyAndAggregate(question, gpt);
trace.push({ round: "retry-gpt", gpt, verifiers: { gpt: gptVerification } });
} catch (error) {
if (debug) console.error("[gpt:retry:error]", error);
}
}
합의 판단과 최종 선택
// 1) 한쪽만 성공한 경우
if (gpt && !claude) {
return {
content: gpt.answer,
sources: extractSources(gpt.reasoning, gpt.answer),
meta: { picked: "gpt", confidence: gpt.confidence },
...(debug ? { debug: { trace } } : {})
};
}
// 2) 수학 문제: 세트 동치 비교
if (/(더해서|합이|sum)/i.test(question)) {
const gptCanonical = canonicalizePairs(`${gpt.answer} ${gpt.reasoning ?? ""}`);
const claudeCanonical = canonicalizePairs(`${claude.answer} ${claude.reasoning ?? ""}`);
if (gptCanonical !== "[]" && gptCanonical === claudeCanonical) {
const pick = (gptVerification.score >= claudeVerification.score) ? "gpt" : "claude";
const chosen = pick === "gpt" ? gpt : claude;
return {
content: chosen.answer,
sources: extractSources(gpt.reasoning, gpt.answer, claude.reasoning, claude.answer),
meta: { picked: pick, agreement: "equivalent", confidence: chosen.confidence },
...(debug ? { debug: { trace } } : {})
};
}
}
// 3) 일반 문제: Judge AI 판정
const verdict = await judgeOpenAI(
question,
`${gpt.answer}\n\n${gpt.reasoning ?? ""}`,
`${claude.answer}\n\n${claude.reasoning ?? ""}`,
{ apiKey: keys?.openai }
);
if (verdict.relation === "equivalent") {
const pick = (gptVerification.score >= claudeVerification.score) ? "gpt" : "claude";
const chosen = pick === "gpt" ? gpt : claude;
return {
content: chosen.answer,
sources: extractSources(chosen.reasoning, chosen.answer),
meta: { picked: pick, agreement: "equivalent", confidence: chosen.confidence }
};
}
// 4) 서로 다른 경우: 점수와 Judge 판정을 종합
const gptScore = (gptVerification.score ?? 0) + (gpt.confidence ?? 0);
const claudeScore = (claudeVerification.score ?? 0) + (claude.confidence ?? 0);
let final: Answer, picked: "gpt" | "claude";
if (verdict.winner === "A") {
final = gpt; picked = "gpt";
} else if (verdict.winner === "B") {
final = claude; picked = "claude";
} else {
// tie인 경우 점수로 결정
final = gptScore >= claudeScore ? gpt : claude;
picked = gptScore >= claudeScore ? "gpt" : "claude";
}
return {
content: final.answer,
sources: extractSources(final.reasoning, final.answer),
meta: { picked, agreement: "different", confidence: final.confidence }
};
🛡️ Security Layer: 생산 환경을 위한 보안
JWT 기반 인증 시스템
// JWT 토큰 생성과 검증
class AuthManager {
private static readonly JWT_SECRET = process.env.JWT_SECRET!;
private static readonly JWT_EXPIRES_IN = '24h';
static generateToken(user: { id: number; email: string }): string {
return jwt.sign(
{
id: user.id,
email: user.email,
iat: Math.floor(Date.now() / 1000)
},
AuthManager.JWT_SECRET,
{ expiresIn: AuthManager.JWT_EXPIRES_IN }
);
}
static verifyToken(token: string): { id: number; email: string } | null {
try {
return jwt.verify(token, AuthManager.JWT_SECRET) as { id: number; email: string };
} catch {
return null;
}
}
}
// 인증 미들웨어
export const authRequired = (req: Request, res: Response, next: NextFunction) => {
const token = req.cookies.cv_token;
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
const user = AuthManager.verifyToken(token);
if (!user) {
res.clearCookie('cv_token');
return res.status(401).json({ error: 'Invalid token' });
}
req.user = user;
next();
};
CSRF Double-Submit Cookie 보호
// server.ts에서 CSRF 보호 구현
app.use((req, res, next) => {
const existingToken = req.cookies["XSRF-TOKEN"];
const token = existingToken || crypto.randomBytes(16).toString("hex");
// 토큰이 없으면 쿠키에 설정
if (!existingToken) {
res.cookie("XSRF-TOKEN", token, {
httpOnly: false, // 클라이언트에서 읽을 수 있어야 함
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
});
}
// POST/PUT/DELETE 요청에서는 헤더 검증
if (["POST", "PUT", "PATCH", "DELETE"].includes(req.method)) {
const headerToken = req.get("X-CSRF-TOKEN");
if (!headerToken || headerToken !== token) {
return res.status(403).json({ error: "CSRF validation failed" });
}
}
next();
});
AES-256-GCM 키 암호화
// 사용자 API 키를 안전하게 저장
class KeyVault {
private static readonly ALGORITHM = 'aes-256-gcm';
private readonly masterKey: Buffer;
constructor() {
this.masterKey = crypto.scryptSync(
process.env.ENCRYPTION_PASSWORD!,
process.env.ENCRYPTION_SALT!,
32
);
}
encrypt(plaintext: string): EncryptedData {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipher(KeyVault.ALGORITHM, this.masterKey);
cipher.setAutoPadding(true);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
}
decrypt(encryptedData: EncryptedData): string {
const decipher = crypto.createDecipher(KeyVault.ALGORITHM, this.masterKey);
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
Rate Limiting
import rateLimit from "express-rate-limit";
// API 키 관련 엔드포인트에 더 엄격한 제한
const keysLimiter = rateLimit({
windowMs: 60_000, // 1분
max: 10, // 최대 10회
message: { error: "Too many key management requests" }
});
app.use("/api/auth/me/keys", keysLimiter);
// 일반 API에는 더 관대한 제한
const generalLimiter = rateLimit({
windowMs: 60_000, // 1분
max: 100, // 최대 100회
});
app.use("/api/", generalLimiter);
🔧 Provider System: AI 모델 호출의 정교함
OpenAI Provider: Responses API vs Chat Completions
export async function askOpenAI(
question: string,
opts?: {
apiKey?: string;
model?: string;
useResponses?: boolean; // Responses API 사용 여부
reasoningEffort?: "minimal" | "medium" | "high";
verbosity?: "low" | "medium" | "high";
}
): Promise<Answer> {
const model = opts?.model ?? process.env.OPENAI_MODEL ?? "gpt-5";
const useResponses = opts?.useResponses ?? (process.env.OPENAI_USE_RESPONSES === "1");
// Responses API 사용 (권장)
if (useResponses) {
const payload: any = {
model,
input: [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: buildUserPrompt(question) },
],
};
// 추론 설정 (Extended Thinking 등)
if (opts?.reasoningEffort) {
payload.reasoning = { effort: opts.reasoningEffort };
}
if (opts?.verbosity) {
payload.text = { verbosity: opts.verbosity };
}
const response = await client.responses.create(payload);
return parseResponseOutput(response);
}
// Chat Completions API (호환성)
const messages = [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: buildUserPrompt(question) },
];
const response = await client.chat.completions.create({
model,
messages,
response_format: { type: "json_object" },
// GPT-5는 temperature 미지원 → 조건부 설정
...(!/gpt-5/i.test(model) && { temperature: 0.7 })
});
return parseChatOutput(response);
}
Anthropic Provider: Extended Thinking 지원
export async function askAnthropic(
question: string,
opts?: {
apiKey?: string,
model?: string,
thinkingBudget?: number; // Extended Thinking 예산
maxTokens?: number;
}
): Promise<Answer> {
const model = opts?.model ?? "claude-sonnet-4-20250514";
const thinkingBudget = Math.max(0, Number(opts?.thinkingBudget ?? 0));
let maxTokens = Math.max(1, Number(opts?.maxTokens ?? 800));
// thinking budget보다 max_tokens가 작으면 자동 보정
if (thinkingBudget > 0 && maxTokens <= thinkingBudget) {
const headroom = Number(process.env.ANTHROPIC_OUTPUT_HEADROOM || 512);
maxTokens = thinkingBudget + headroom;
if (process.env.NODE_ENV !== "production") {
console.warn(`max_tokens auto-bumped to ${maxTokens} (budget=${thinkingBudget})`);
}
}
const client = new Anthropic({ apiKey: opts?.apiKey ?? process.env.ANTHROPIC_API_KEY! });
const params: any = {
model,
system: SYSTEM_PROMPT,
max_tokens: maxTokens,
messages: [{ role: "user", content: buildUserPrompt(question) }]
};
// Extended Thinking 설정
if (thinkingBudget > 0) {
params.thinking = {
type: "enabled",
budget_tokens: thinkingBudget
};
}
// Claude Sonnet 4는 temperature 미지원
const response = await client.messages.create(params);
return parseAnthropicOutput(response);
}
🧪 디버깅과 관찰성
상세한 추적 로그
type TraceEntry = {
round: number | string;
gpt?: Answer;
claude?: Answer;
verifiers?: {
gpt?: VerificationResult;
claude?: VerificationResult;
};
consensus?: ConsensusResult;
verdict?: Verdict;
};
// orchestrate 함수에서 각 단계별로 추적
const trace: TraceEntry[] = [];
// 1차 호출 결과
trace.push({
round: 1,
gpt,
claude,
verifiers: { gpt: gptVerification, claude: claudeVerification }
});
// 재시도가 있었다면
if (gptRetried) {
trace.push({
round: "retry-gpt",
gpt: retriedGptAnswer,
verifiers: { gpt: retriedGptVerification }
});
}
// Judge 판정 결과
if (verdict) {
trace.push({ verdict });
}
// 최종 결과에 디버그 정보 포함
return {
content: finalAnswer.answer,
sources: extractSources(...),
meta: { picked, agreement, confidence },
...(debug ? { debug: { trace } } : {})
};
성능 메트릭
// 각 단계별 소요 시간 측정
const startTime = Date.now();
const timings: Record<string, number> = {};
timings.modelCall = Date.now() - startTime;
const verifyStart = Date.now();
// 검증 로직...
timings.verification = Date.now() - verifyStart;
const judgeStart = Date.now();
// 판정 로직...
timings.judgment = Date.now() - judgeStart;
if (debug) {
console.log("Performance metrics:", timings);
}
🔮 다음 편 예고
3편에서는 React 기반 프론트엔드와 전체 사용자 경험을 다룹니다:
- 🎨 UI/UX 설계: 복잡한 검증 과정을 직관적으로 표현하기
- 🔄 실시간 피드백: 로딩 상태와 프로그레스 표시
- 📊 디버그 뷰어: 전체 검증 과정의 시각화
- 🔐 프론트엔드 보안: CSRF 토큰 자동 처리
- 🚀 최적화와 성능: 코드 스플리팅과 캐싱 전략
핵심 질문: "복잡한 백엔드 프로세스를 사용자가 쉽고 직관적으로 사용할 수 있게 만들려면?"
2편에서 MeshProof의 핵심 엔진을 해부해봤습니다.
다음 편에서는 이 모든 기능을 사용자가 실제로 어떻게 경험하게 되는지 살펴보겠습니다.
'MyStory > Consensus_Verifiers' 카테고리의 다른 글
| MeshProof: 두 모델이 합의할 때까지 수렴하는 QA 시스템(3) (0) | 2025.08.24 |
|---|---|
| MeshProof: 두 모델이 합의할 때까지 수렴하는 QA 시스템 (0) | 2025.08.24 |