잡담
[개발기] 에이전트끼리 토론시키기 — Roundtable 설계와 한계
지나가던행인이

Lv.1 지나가던행인이 (61.♡.201.240)

2026년 4월 15일 AM 11:35

조회 880 공감 0

tunaFlow 기술 포스트 시리즈 4편입니다. 3편에서 Branch 구조를 다뤘는데, Roundtable(RT)은 Branch의 확장 모드입니다. branches.mode = "roundtable"이면 여러 에이전트가 하나의 주제를 놓고 토론합니다.


왜 에이전트끼리 토론을 시키는가

에이전트에게 코드 리뷰를 시키면 대부분 "좋습니다"라고 합니다. 한 에이전트에게 설계를 맡기면 자기가 제안한 방향에 대해 스스로 비판하는 것이 구조적으로 어렵습니다.

사람 팀에서 코드 리뷰가 작동하는 이유는 리뷰어가 작성자와 다른 사람이기 때문입니다. 관점이 다르고, 놓치는 부분이 다릅니다. 에이전트에도 같은 구조를 적용할 수 있습니다 — 다른 엔진, 다른 역할, 다른 프롬프트를 가진 에이전트 여럿이 같은 주제를 보면, 한 에이전트가 놓치는 것을 다른 에이전트가 잡아냅니다.

RT는 이 아이디어의 구현입니다.


RT = Branch의 확장 모드

RT는 별도의 시스템이 아니라 Branch의 모드입니다.

// branches 테이블
pub struct Branch {
    pub mode: String,  // "chat" 또는 "roundtable"
    // ... 나머지는 일반 Branch와 동일
}

RT를 생성하면 mode = "roundtable"인 Branch가 만들어지고, shadow conversation에 참가자들의 메시지가 쌓입니다. 사이드바에서는 Roundtables 섹션에 표시되고, 드로어에서 열리고, adopt도 동일하게 동작합니다. 3편에서 다룬 Branch 인프라를 그대로 씁니다.

다른 점은 메시지를 사용자가 보내는 게 아니라 참가자들이 순서대로(또는 동시에) 보낸다는 것뿐입니다.


두 가지 토론 모드

Sequential — 순차 토론

참가자가 한 명씩 순서대로 발언합니다. 뒤의 참가자는 앞 참가자가 뭘 말했는지 볼 수 있습니다.

Round 1:
  Architect (claude)  → 설계 제안: "모듈 3개로 분리하자"
  Reviewer (codex)    → Architect 응답을 보고: "모듈 경계가 애매하다"
  Verifier (gemini)   → 둘 다 보고: "Reviewer 의견에 동의, 2번 모듈이 문제"
// sequential.rs — 핵심 루프
for p in participants {
    let prompt = if p.blind {
        // blind 참가자: 다른 참가자 응답을 안 봄
        build_round_prompt_with_identity(topic, &[], &[], Some(&identity))
    } else {
        // 앞 참가자들의 응답을 포함
        build_round_prompt_with_identity(topic, transcript, &round_responses, Some(&identity))
    };
    let result = stream_participant(p, prompt, ...).await;
    round_responses.push((p.name.clone(), result.content.clone()));
}

순차 모드의 장점은 대화가 자연스럽게 쌓인다는 것입니다. 뒤의 참가자가 앞의 논점을 반박하거나 보완할 수 있습니다.

Deliberative — 병렬 토론

참가자가 동시에 발언합니다. 서로의 응답을 보지 못합니다.

Round 1:
  Architect (claude)  → (독립적으로) "모듈 3개 분리"
  Reviewer (codex)    → (독립적으로) "인터페이스 추상화 필요"
  Verifier (gemini)   → (독립적으로) "테스트 커버리지 부족"
// deliberative.rs — tokio::spawn으로 병렬 실행
for p in participants {
    let prompt = build_round_prompt_with_identity(topic, transcript, &[], Some(&identity));
    //                                                                 ^^^ 현재 라운드 응답 없음
    tokio::spawn(async move {
        let result = stream_participant(p, prompt, ...).await;
        tx.send((msg_id, result)).await;
    });
}
// 완료 순서대로 결과 수집
while let Some((id, result)) = rx.recv().await { ... }

병렬 모드의 장점은 독립적인 관점입니다. 앞 참가자의 의견에 끌려가지 않으므로 진짜 다른 시각이 나옵니다. 단점은 논점이 분산될 수 있다는 것입니다.

어떤 모드를 언제 쓰는가

상황

추천 모드

이유

코드 리뷰

Sequential

Reviewer가 Developer 결과를 보고 평가해야 함

설계 브레인스토밍

Deliberative

독립적 아이디어가 필요

보안 감사

Deliberative + blind

선입견 없이 독립 평가

합의 도출

Sequential 2라운드

1라운드: 독립 의견, 2라운드: 상호 반응


참가자 실행 모델

각 참가자는 독립 서브프로세스로 실행됩니다. PTY 세션 공유가 아니라 참가자마다 별도 -p 모드 호출입니다.

RT 실행:
  Participant A (claude) → claude -p "..." → subprocess 1
  Participant B (codex)  → codex exec "..." → subprocess 2
  Participant C (gemini) → gemini -p "..." → subprocess 3

이 설계의 이유: RT 참가자는 서로 다른 엔진일 수 있습니다. Claude, Codex, Gemini를 섞어서 토론시키는 것이 RT의 핵심 가치입니다. PTY 세션은 엔진별로 하나이므로 공유가 구조적으로 불가능합니다.

엔진별 실행 핸들러:

엔진

실행 방식

스트리밍

Claude

claude::stream_run()

실시간 chunk

Codex

codex::stream_run()

실시간 chunk

Gemini

gemini::stream_run()

실시간 chunk

Ollama

openai_compat::stream_run()

async 네이티브

OpenCode

opencode::run()

비스트리밍

실행 중에 roundtable:chunk 이벤트가 프론트로 스트리밍되어, 각 참가자의 응답이 실시간으로 드로어에 표시됩니다.


맥락 관리: 최소 맥락 전략

RT 참가자에게 전체 ContextPack(~5-7k 토큰)을 넘기면 토큰 낭비입니다. 참가자가 3명이면 3배, 2라운드면 6배. 그래서 RT는 최소 맥락 전략을 씁니다.

RtContextCache — Tier 0+1만

// context.rs
pub struct RtContextCache {
    pub context: Option<String>,  // ~100-200 토큰
}

ContextPack의 12개 섹션 중 프로젝트 경로 + 활성 Plan 요약만 포함합니다. Skills, retrieval, compressed memory, cross-session 등은 전부 제외. RT 참가자에게는 "지금 어떤 프로젝트에서 어떤 작업을 하고 있는지" 정도만 알려주면 충분합니다.

벡터 검색으로 이전 라운드 압축

2라운드 이상에서 이전 라운드 전체 transcript를 넘기면 토큰이 급증합니다. 참가자 3명의 응답이 각 4,000자라면 12,000자(~3,000 토큰)가 됩니다.

RtVectorIndex는 이전 라운드 응답을 임베딩해두고, 현재 토픽과 관련된 상위 5개 청크만 추출합니다.

// context.rs
pub fn search(&self, topic: &str, limit: usize) -> Vec<(String, String)> {
    // 코사인 유사도 기반 검색, 0.2 이상만
    // 각 청크 800자로 truncate
}

결과: 12,000자 → 2,400자. 토큰 ~80% 절감. rawq 데몬이 꺼져 있으면 전체 transcript로 폴백합니다.


역할 시스템

참가자에게 역할을 부여하면 프롬프트에 역할별 가이드라인이 주입되고, 출력 토큰이 제한됩니다.

4가지 역할

fn role_guidance(role: &str) -> &'static str {
    match role {
        "proposer" => "결론을 먼저 제시하고 근거를 대라...",
        "reviewer" | "critic" => "4개 차원(plan_coverage, code_quality, test_coverage, convention)으로 평가하라...",
        "verifier" | "judge" => "다른 참가자 평가에 의존하지 말고 독립적으로 판단하라...",
        "synthesizer" | "lead" => "consensus/contested/dissent 3섹션으로 정리하라...",
        _ => "",
    }
}

역할별 출력 제한

역할

기본 토큰 제한

이유

proposer

1,200

핵심 제안만

reviewer/critic

900

평가에 집중

verifier/judge

800

판정은 짧아야 함

synthesizer/lead

2,000

종합은 길어도 됨

토큰 제한은 프롬프트 앞에 [Output limit: Keep your response under approximately N tokens.]로 주입됩니다. 강제가 아니라 지시이지만, 대부분의 모델이 잘 따릅니다.

Blind 참가자

blind: true로 설정하면 해당 참가자는 토픽과 자신의 역할만 받고, 다른 참가자의 응답을 전혀 보지 못합니다.

if p.blind {
    build_round_prompt_with_identity(topic, &[], &[], Some(&identity))
    //                                       ^^   ^^ transcript와 현재 라운드 모두 빈 배열
}

보안 감사나 독립 검증이 필요할 때 유용합니다. "다른 리뷰어가 pass했으니 나도 pass"하는 편향을 구조적으로 차단합니다.


Review RT: 워크플로우에서의 활용

2편에서 다룬 워크플로우 파이프라인에서, Review 단계는 RT를 사용합니다.

impl-complete 감지
  → Review Branch 생성 (mode: "roundtable")
  → Reviewer + Verifier 2명 실행
  → verdict 도출 (pass/fail/conditional)

Review RT가 2명인 이유: 역할이 명확히 다르기 때문입니다. Reviewer는 코드 품질/테스트/컨벤션을 수치로 평가하고, Verifier는 Reviewer의 평가 자체를 독립적으로 검증합니다. 3명 이상이면 토큰 비용만 올라가고 추가 관점이 제한적입니다. 설계 토론과 달리 리뷰는 기준이 정해져 있어서 관점 다양성보다 정확성이 중요합니다.


Brief: 토론 결과 요약

RT가 끝나면 rule-based 요약(brief)이 자동 생성됩니다. LLM을 추가 호출하지 않습니다.

// persist.rs — save_shared_brief
for name in &unique_names {
    if let Some((_, content)) = transcript.iter().rev().find(|(n, _)| n == name) {
        let summary = first_sentences(content, 2);  // 첫 2문장, 300자 제한
        position_lines.push(format!("- **{}**: {}", name, summary));
    }
}

각 참가자의 마지막 응답에서 첫 2문장을 추출하여 Key Positions로 정리합니다. Brief는 memos 테이블에 type = 'roundtable_brief'로 저장되고, adopt 시 이 Key Positions가 메인 대화에 삽입됩니다.

LLM 요약을 쓰지 않는 이유: RT 자체가 이미 토큰을 상당히 소비하는데(3명 1라운드 기준 ~10k-20k 토큰), 요약에 추가 호출을 하면 비용이 더 늘어납니다. 첫 2문장 추출이 조잡하지만, RT 참가자들이 "결론을 먼저 말하라"는 역할 가이드라인을 따르면 첫 2문장이 실제로 핵심 입장이 됩니다.


실사용 패턴

라운드 운영

3명 토론에서 흔한 패턴은 최소 5라운드입니다. 에이전트만 토론하는 게 아니라 사용자가 중간에 끼어들어 방향을 정리하기 때문입니다. 사용자의 개입도 라운드에 포함됩니다.

Round 1: 에이전트 3명 각자 의견 제시
Round 2: 사용자 — "A 방향이 유력한데, B의 우려도 반영해서 다시 검토해봐"
Round 3: 에이전트 3명 — 상대방 의견 검토 + 자기 의견 수정/보완
Round 4: 사용자 — "이 부분은 합의된 거 같고, 나머지 쟁점에 집중해"
Round 5: 에이전트 3명 — 최종 의견 + synthesizer 정리

사용자가 2번 끼면 실제 에이전트 토론은 3라운드입니다. 이 정도면 대부분 수렴합니다. 참가자들은 상대방 의견에 동의하기도 하고, 자기 제안을 수정하기도 하고, 상대방 제안을 보완해주기도 합니다. "내 의견을 유지합니다"만 반복하는 경우는 생각보다 적습니다.

복잡한 설계 토론에서는 사용자가 방향을 여러 번 정리하면서 8-10라운드까지 가는 경우도 있습니다. synthesizer는 마지막에 결정된 부분과 안 된 부분을 요약하는 역할이고, 합의가 안 되는 쟁점이 남으면 사용자가 방향을 판단합니다.

간단하거나 방향이 이미 정해진 내용은 RT까지 갈 필요 없이 Architect와 1:1 대화로 충분합니다.

실제 사례

tunaFlow 개발에서 RT가 유효했던 사례가 있습니다. tunapi(API 프로젝트)와 seCall(통화 분석 프로젝트)의 LLM Wiki 구현체를 설계할 때, 3명 토론으로 데이터 구조, 인덱싱 전략, 검색 알고리즘을 논의했습니다. 에이전트들이 각자 다른 접근을 제안하고, 라운드를 거치면서 서로의 제안을 검토하고 보완한 결과가 실제 구현에 그대로 반영되었습니다. 혼자 Architect와 대화했으면 한 방향만 깊이 파고 다른 가능성을 놓쳤을 부분들이, 토론을 통해 비교 검토된 것입니다.

사용자의 역할

RT에서 사용자는 사회자입니다. 에이전트들이 알아서 합의에 도달하기를 기대하는 것이 아니라, 라운드 사이에 방향을 잡아주고 쟁점을 정리해주면 토론 품질이 올라갑니다. "이 방향으로 좁혀봐", "A의 제안과 B의 우려를 둘 다 반영하는 안을 내봐" 같은 한 줄이 다음 라운드의 품질을 크게 바꿉니다.


현재 한계

1. 토큰 비용이 라운드에 비례

참가자 N명 × R라운드면 N×R번의 LLM 호출이 발생합니다. 3명 3라운드면 9번. 이전 라운드 transcript까지 포함되므로 라운드가 진행될수록 입력 토큰도 증가합니다. 벡터 검색으로 이전 라운드 맥락을 ~80% 절감하지만, 근본적으로 N과 R에 비례하는 비용 구조입니다.

다만 토론 비용이 높더라도, 잘못된 설계를 나중에 고치는 비용에 비하면 RT 3라운드의 토큰 비용은 합리적입니다. 문제는 비용을 미리 예측하기 어렵다는 것입니다. 3라운드로 끝날지 8라운드로 갈지는 주제 복잡도에 따라 다릅니다.

2. Sequential 모드의 시각적 한계

Sequential 토론에서 참가자 B의 응답을 보면, 분명히 참가자 A의 의견을 보고 반응하고 있습니다. 앞선 발언을 인용하거나 반박하거나 보완합니다. 하지만 UI에서는 "순서대로 나오는 응답"으로 보이기 때문에, 참가자들이 실제로 상호작용하고 있다는 것이 시각적으로 잘 드러나지 않습니다.

참가자 B의 프롬프트에 A의 전체 응답이 들어가고, B가 그것을 소화한 뒤 자기 응답을 시작하는데, 이 "소화" 과정은 LLM 내부에서 일어납니다. 결과물을 읽어보면 상호 참조가 잘 되어 있지만, 실시간으로 보면 독립 응답처럼 느껴질 수 있습니다.


핵심 결정 정리

결정

이유

RT = Branch 모드

별도 시스템 불필요. Branch 인프라(shadow conv, 드로어, adopt) 재활용

참가자별 독립 서브프로세스

다른 엔진 혼용 가능. PTY 세션 공유 구조적 불가

최소 맥락 (RtContextCache)

참가자 수 × 라운드 수만큼 맥락 비용 증가. Tier 0+1만

벡터 검색으로 transcript 압축

이전 라운드 전체를 넘기면 토큰 폭증. ~80% 절감

역할별 출력 제한

장황한 응답 방지. proposer 1200, reviewer 900, synthesizer 2000

rule-based brief (LLM 미사용)

RT 자체가 이미 토큰 소비 큼. 추가 호출 비용 회피

Review RT는 2명

리뷰는 관점 다양성보다 정확성. Reviewer + 독립 Verifier 구조


다음 편 예고

5편: "대화가 길어지면" — 에이전트 장기 메모리 구현기

에이전트와 오래 대화하면 맥락 창이 넘칩니다. tunaFlow는 compressed memory, 벡터 검색, cross-session 링크로 장기 메모리를 구현합니다. 압축 타이밍, 검색 품질, 그리고 "에이전트가 과거를 어디까지 기억해야 하는가"의 설계 판단을 다룹니다.


시리즈 목록

  1. [에이전트에게 프로세스를 줘라] - AOC를 만들면서 배운 것

  2. [Plan → Dev → Review]— 워크플로우 파이프라인 구현기

  3. 대화를 분기한다 — Branch 설계와 활용

  4. 에이전트끼리 토론시키기 — Roundtable 설계와 한계 (이 글)

  5. 대화가 길어지면 — 에이전트 장기 메모리 구현기

  6. Claude $20으로 워크플로우 돌리기 — 엔진 아키텍처

  7. 코드 구조를 에이전트에게 알려주기 — rawq + code-review-graph

  8. 246개 스킬 중 필요한 것만 — 스킬 자동 적용 구현기

  9. 에이전트가 같은 실수를 반복하면 — 품질 보증 설계

  10. tunaFlow로 풀사이클 돌려보기 — 워크플로우 실전 테스트 회고

  • [기 발행] ContextPack — 멀티 에이전트 오케스트레이터의 컨텍스트 설계


레퍼런스

  • tunaFlow 내부 문서

    • RT 실행 엔진: src-tauri/src/commands/roundtable_helpers/executor.rs

    • Sequential 모드: src-tauri/src/commands/roundtable_helpers/sequential.rs

    • Deliberative 모드: src-tauri/src/commands/roundtable_helpers/deliberative.rs

    • 맥락 캐시: src-tauri/src/commands/roundtable_helpers/context.rs

    • 역할 가이드: src-tauri/src/commands/roundtable_helpers/types.rs

    • 프롬프트 조립: src-tauri/src/commands/roundtable_helpers/prompt.rs

    • Brief 생성: src-tauri/src/commands/roundtable_helpers/persist.rs

댓글 (0)

  • 아직 댓글이 없습니다. 첫 댓글을 작성해보세요!

댓글을 작성하려면 이 필요합니다.