DEV Community

Cover image for LLM 배치 처리로 100개 이상 에이전트 설정 생성 방법
Rihpig
Rihpig

Posted on Originally published at apidog.com

LLM 배치 처리로 100개 이상 에이전트 설정 생성 방법

소개

소셜 미디어 시뮬레이션에서 수백 개의 AI 에이전트 구성을 자동화하려면 시간, 이벤트, 활동 패턴, 응답 지연 등 다양한 속성을 체계적으로 정의해야 합니다. 수동으로 처리하면 시간과 오류가 크게 증가합니다.

Apidog를 지금 바로 사용해보세요

MiroFish는 LLM 기반 구성 자동 생성을 통해 이 과정을 단축합니다. 시스템은 시뮬레이션 요구사항, 문서, 지식 그래프를 입력받아 각 에이전트의 세부 속성을 생성합니다.

하지만 LLM의 한계(잘린 출력, JSON 형식 오류, 토큰 제한 등)로 인해 다음과 같은 구현 전략이 필요합니다:

  • 단계별 구성 생성 (시간 → 이벤트 → 에이전트 → 플랫폼)
  • 배치 처리로 컨텍스트 초과 방지
  • JSON 복구 및 오류 처리
  • LLM 실패 시 규칙 기반 기본값 적용
  • 유형별 에이전트 활동 패턴 적용
  • 생성값 유효성 검사 및 자동 수정

💡 구성 생성 파이프라인은 100개 이상의 에이전트에 대해 일련의 API 호출을 수행합니다. Apidog는 각 단계의 요청/응답 스키마를 검증하고, 프로덕션 전 JSON 오류를 포착하며, 잘린 LLM 응답 등 예외 케이스 테스트에 활용합니다.

모든 코드는 MiroFish 실제 사용 사례에서 발췌했습니다.

아키텍처 개요

구성 생성기는 파이프라인 구조로 동작합니다:

┌────────────┐     ┌───────────────┐     ┌───────────────┐
│ Context    │ ──► │ Time Config   │ ──► │ Event Config  │
│ Builder    │     │ Generator     │     │ Generator     │
│            │     │               │     │               │
└────────────┘     └───────────────┘     └───────────────┘
        │                                       │
        ▼                                       ▼
 ┌─────────────────┐     ┌────────────────────┐
 │   Agent Config  │ ──► │   Platform Config │
 │   (Batch)       │     │                   │
 └─────────────────┘     └────────────────────┘
Enter fullscreen mode Exit fullscreen mode

파일 구조

backend/app/services/
├── simulation_config_generator.py  # 메인 구성 생성 로직
├── ontology_generator.py           # 온톨로지 생성(공통)
└── zep_entity_reader.py            # 개체 필터링

backend/app/models/
├── task.py                         # 작업 추적
└── project.py                      # 프로젝트 상태
Enter fullscreen mode Exit fullscreen mode

단계별 생성 전략

한 번에 모든 에이전트 구성을 생성하면 LLM 토큰 한계를 초과합니다. 따라서 15개 단위로 배치 처리합니다.

class SimulationConfigGenerator:
    AGENTS_PER_BATCH = 15
    MAX_CONTEXT_LENGTH = 50000
    TIME_CONFIG_CONTEXT_LENGTH = 10000
    EVENT_CONFIG_CONTEXT_LENGTH = 8000
    ENTITY_SUMMARY_LENGTH = 300
    AGENT_SUMMARY_LENGTH = 300
    ENTITIES_PER_TYPE_DISPLAY = 20

    def generate_config(...):
        num_batches = math.ceil(len(entities) / self.AGENTS_PER_BATCH)
        total_steps = 3 + num_batches
        current_step = 0

        def report_progress(step: int, message: str):
            ...

        # 1. 컨텍스트 구축
        context = self._build_context(...)

        # 2. 시간 구성 생성
        report_progress(1, "시간 구성 생성 중...")
        time_config_result = self._generate_time_config(context, len(entities))
        time_config = self._parse_time_config(time_config_result, len(entities))

        # 3. 이벤트 구성 생성
        report_progress(2, "이벤트/핫토픽 구성 생성 중...")
        event_config_result = self._generate_event_config(context, simulation_requirement, entities)
        event_config = self._parse_event_config(event_config_result)

        # 4. 에이전트 배치 구성 생성
        all_agent_configs = []
        for batch_idx in range(num_batches):
            ...
            batch_configs = self._generate_agent_configs_batch(...)
            all_agent_configs.extend(batch_configs)

        # 5. 초기 게시물 발행자 할당
        event_config = self._assign_initial_post_agents(event_config, all_agent_configs)

        # 6. 플랫폼 구성
        report_progress(total_steps, "플랫폼 구성 생성 중...")
        twitter_config = PlatformConfig(platform="twitter", ...) if enable_twitter else None
        reddit_config = PlatformConfig(platform="reddit", ...) if enable_reddit else None

        # 7. 최종 구성 반환
        params = SimulationParameters(...)
        return params
Enter fullscreen mode Exit fullscreen mode

주요 효과

  • 각 LLM 호출의 컨텍스트를 제한하여 실패율 감소
  • 사용자 진행률 표시 지원
  • 부분 실패 시 재시도 및 복구 가능

컨텍스트 구축

토큰 한도를 초과하지 않으면서 시뮬레이션 요구, 개체 요약, 문서 일부를 포함합니다.

def _build_context(self, simulation_requirement, document_text, entities) -> str:
    entity_summary = self._summarize_entities(entities)
    context_parts = [
        f"## Simulation Requirement\n{simulation_requirement}",
        f"\n## Entity Information ({len(entities)} entities)\n{entity_summary}",
    ]
    current_length = sum(len(p) for p in context_parts)
    remaining_length = self.MAX_CONTEXT_LENGTH - current_length - 500
    if remaining_length > 0 and document_text:
        doc_text = document_text[:remaining_length]
        if len(document_text) > remaining_length:
            doc_text += "\n...(document truncated)"
        context_parts.append(f"\n## Original Document\n{doc_text}")
    return "\n".join(context_parts)
Enter fullscreen mode Exit fullscreen mode

개체 요약

개체는 유형별로 최대 20개만 노출하고 내용을 요약합니다.

def _summarize_entities(self, entities):
    lines = []
    by_type = {}
    for e in entities:
        t = e.get_entity_type() or "Unknown"
        by_type.setdefault(t, []).append(e)
    for entity_type, type_entities in by_type.items():
        lines.append(f"\n### {entity_type} ({len(type_entities)} entities)")
        for e in type_entities[:self.ENTITIES_PER_TYPE_DISPLAY]:
            summary_preview = (e.summary[:self.ENTITY_SUMMARY_LENGTH] + "...") if len(e.summary) > self.ENTITY_SUMMARY_LENGTH else e.summary
            lines.append(f"- {e.name}: {summary_preview}")
        if len(type_entities) > self.ENTITIES_PER_TYPE_DISPLAY:
            lines.append(f"  ... and {len(type_entities) - self.ENTITIES_PER_TYPE_DISPLAY} more")
    return "\n".join(lines)
Enter fullscreen mode Exit fullscreen mode

출력 예시:

### Student (45 entities)
- Zhang Wei: Active in student union, frequently posts about campus events...
... and 43 more

### University (3 entities)
- Wuhan University: Official account, posts announcements and news...
Enter fullscreen mode Exit fullscreen mode

시간 구성 생성

중국 시간대 기반의 활동 패턴을 자동 생성합니다.

def _generate_time_config(self, context, num_entities):
    context_truncated = context[:self.TIME_CONFIG_CONTEXT_LENGTH]
    max_agents_allowed = max(1, int(num_entities * 0.9))
    prompt = f"""... (중략) ..."""
    system_prompt = "You are a social media simulation expert. Return pure JSON format."
    try:
        return self._call_llm_with_retry(prompt, system_prompt)
    except Exception as e:
        logger.warning(f"Time config LLM generation failed: {e}, using default")
        return self._get_default_time_config(num_entities)
Enter fullscreen mode Exit fullscreen mode

시간 구성 파싱/유효성 검사

def _parse_time_config(self, result, num_entities):
    agents_per_hour_min = result.get("agents_per_hour_min", max(1, num_entities // 15))
    agents_per_hour_max = result.get("agents_per_hour_max", max(5, num_entities // 5))
    if agents_per_hour_min > num_entities:
        agents_per_hour_min = max(1, num_entities // 10)
    if agents_per_hour_max > num_entities:
        agents_per_hour_max = max(agents_per_hour_min + 1, num_entities // 2)
    if agents_per_hour_min >= agents_per_hour_max:
        agents_per_hour_min = max(1, agents_per_hour_max // 2)
    return TimeSimulationConfig(
        total_simulation_hours=result.get("total_simulation_hours", 72),
        minutes_per_round=result.get("minutes_per_round", 60),
        agents_per_hour_min=agents_per_hour_min,
        agents_per_hour_max=agents_per_hour_max,
        peak_hours=result.get("peak_hours", [19, 20, 21, 22]),
        off_peak_hours=result.get("off_peak_hours", [0, 1, 2, 3, 4, 5]),
        off_peak_activity_multiplier=0.05,
        morning_activity_multiplier=0.4,
        work_activity_multiplier=0.7,
        peak_activity_multiplier=1.5
    )
Enter fullscreen mode Exit fullscreen mode

기본 시간 구성

def _get_default_time_config(self, num_entities):
    return {
        "total_simulation_hours": 72,
        "minutes_per_round": 60,
        "agents_per_hour_min": max(1, num_entities // 15),
        "agents_per_hour_max": max(5, num_entities // 5),
        "peak_hours": [19, 20, 21, 22],
        "off_peak_hours": [0, 1, 2, 3, 4, 5],
        "morning_hours": [6, 7, 8],
        "work_hours": [9, 10, 11, 12, 13, 14, 15, 16, 17, 18],
        "reasoning": "Using default Chinese timezone configuration"
    }
Enter fullscreen mode Exit fullscreen mode

이벤트 구성 생성

이벤트 구성은 핫토픽, 내러티브 방향, 초기 게시물(게시자 유형 포함)을 정의합니다.

def _generate_event_config(self, context, simulation_requirement, entities):
    # (중략) entity_types_available, type_examples 생성
    prompt = f"""... (중략) ..."""
    system_prompt = "You are an opinion analysis expert. Return pure JSON format."
    try:
        return self._call_llm_with_retry(prompt, system_prompt)
    except Exception as e:
        logger.warning(f"Event config LLM generation failed: {e}, using default")
        return {
            "hot_topics": [],
            "narrative_direction": "",
            "initial_posts": [],
            "reasoning": "Using default configuration"
        }
Enter fullscreen mode Exit fullscreen mode

초기 게시물 발행자 할당

def _assign_initial_post_agents(self, event_config, agent_configs):
    if not event_config.initial_posts:
        return event_config
    agents_by_type = {}
    for agent in agent_configs:
        etype = agent.entity_type.lower()
        agents_by_type.setdefault(etype, []).append(agent)
    type_aliases = {
        "official": ["official", "university", ...],
        ...
    }
    used_indices = {}
    updated_posts = []
    for post in event_config.initial_posts:
        poster_type = post.get("poster_type", "").lower()
        content = post.get("content", "")
        matched_agent_id = None
        # 1. 직접 매칭
        if poster_type in agents_by_type:
            agents = agents_by_type[poster_type]
            idx = used_indices.get(poster_type, 0) % len(agents)
            matched_agent_id = agents[idx].agent_id
            used_indices[poster_type] = idx + 1
        else:
            # 2. alias 매칭
            for alias_key, aliases in type_aliases.items():
                if poster_type in aliases or alias_key == poster_type:
                    for alias in aliases:
                        if alias in agents_by_type:
                            agents = agents_by_type[alias]
                            idx = used_indices.get(alias, 0) % len(agents)
                            matched_agent_id = agents[idx].agent_id
                            used_indices[alias] = idx + 1
                            break
                    if matched_agent_id is not None:
                        break
        # 3. fallback: 영향력 높은 agent
        if matched_agent_id is None:
            if agent_configs:
                sorted_agents = sorted(agent_configs, key=lambda a: a.influence_weight, reverse=True)
                matched_agent_id = sorted_agents[0].agent_id
            else:
                matched_agent_id = 0
        updated_posts.append({
            "content": content,
            "poster_type": post.get("poster_type", "Unknown"),
            "poster_agent_id": matched_agent_id
        })
    event_config.initial_posts = updated_posts
    return event_config
Enter fullscreen mode Exit fullscreen mode

배치 에이전트 구성 생성

15개 단위로 에이전트 활동 패턴을 생성합니다. 각 유형별로 다른 활동/응답/영향력 패턴을 적용합니다.

def _generate_agent_configs_batch(self, context, entities, start_idx, simulation_requirement):
    entity_list = []
    summary_len = self.AGENT_SUMMARY_LENGTH
    for i, e in enumerate(entities):
        entity_list.append({
            "agent_id": start_idx + i,
            "entity_name": e.name,
            "entity_type": e.get_entity_type() or "Unknown",
            "summary": e.summary[:summary_len] if e.summary else ""
        })
    prompt = f"""... (중략) ..."""
    system_prompt = "You are a social media behavior analysis expert. Return pure JSON format."
    try:
        result = self._call_llm_with_retry(prompt, system_prompt)
        llm_configs = {cfg["agent_id"]: cfg for cfg in result.get("agent_configs", [])}
    except Exception as e:
        logger.warning(f"Agent config batch LLM generation failed: {e}, using rule-based generation")
        llm_configs = {}
    configs = []
    for i, entity in enumerate(entities):
        agent_id = start_idx + i
        cfg = llm_configs.get(agent_id, {})
        if not cfg:
            cfg = self._generate_agent_config_by_rule(entity)
        config = AgentActivityConfig(
            agent_id=agent_id,
            entity_uuid=entity.uuid,
            entity_name=entity.name,
            entity_type=entity.get_entity_type() or "Unknown",
            activity_level=cfg.get("activity_level", 0.5),
            posts_per_hour=cfg.get("posts_per_hour", 0.5),
            comments_per_hour=cfg.get("comments_per_hour", 1.0),
            active_hours=cfg.get("active_hours", list(range(9, 23))),
            response_delay_min=cfg.get("response_delay_min", 5),
            response_delay_max=cfg.get("response_delay_max", 60),
            sentiment_bias=cfg.get("sentiment_bias", 0.0),
            stance=cfg.get("stance", "neutral"),
            influence_weight=cfg.get("influence_weight", 1.0)
        )
        configs.append(config)
    return configs
Enter fullscreen mode Exit fullscreen mode

규칙 기반 대체 구성

LLM 실패 시, 유형별 기본값을 자동 적용합니다.

def _generate_agent_config_by_rule(self, entity):
    entity_type = (entity.get_entity_type() or "Unknown").lower()
    if entity_type in ["university", "governmentagency", "ngo"]:
        return {...}
    elif entity_type in ["mediaoutlet"]:
        return {...}
    elif entity_type in ["professor", "expert", "official"]:
        return {...}
    elif entity_type in ["student"]:
        return {...}
    elif entity_type in ["alumni"]:
        return {...}
    else:
        return {...}
Enter fullscreen mode Exit fullscreen mode

LLM 호출: 재시도 및 JSON 복구

LLM 출력이 잘리거나 JSON 형식이 깨질 경우, 자동으로 재시도 및 복구를 수행합니다.

def _call_llm_with_retry(self, prompt, system_prompt):
    import re
    max_attempts = 3
    last_error = None
    for attempt in range(max_attempts):
        try:
            response = self.client.chat.completions.create(
                model=self.model_name,
                messages=[
                    {"role": "system", "content": system_prompt},
                    {"role": "user", "content": prompt}
                ],
                response_format={"type": "json_object"},
                temperature=0.7 - (attempt * 0.1)
            )
            content = response.choices[0].message.content
            finish_reason = response.choices[0].finish_reason
            if finish_reason == 'length':
                content = self._fix_truncated_json(content)
            try:
                return json.loads(content)
            except json.JSONDecodeError:
                fixed = self._try_fix_config_json(content)
                if fixed:
                    return fixed
                last_error = e
        except Exception as e:
            last_error = e
            import time
            time.sleep(2 * (attempt + 1))
    raise last_error or Exception("LLM call failed")
Enter fullscreen mode Exit fullscreen mode

잘린 JSON 수정/복구

def _fix_truncated_json(self, content):
    content = content.strip()
    open_braces = content.count('{') - content.count('}')
    open_brackets = content.count('[') - content.count(']')
    if content and content[-1] not in '",}]':
        content += '"'
    content += ']' * open_brackets
    content += '}' * open_braces
    return content

def _try_fix_config_json(self, content):
    import re
    content = self._fix_truncated_json(content)
    json_match = re.search(r'\{[\s\S]*\}', content)
    if json_match:
        json_str = json_match.group()
        def fix_string(match):
            s = match.group(0)
            s = s.replace('\n', ' ').replace('\r', ' ')
            s = re.sub(r'\s+', ' ', s)
            return s
        json_str = re.sub(r'"[^"\\]*(?:\\.[^"\\]*)*"', fix_string, json_str)
        try:
            return json.loads(json_str)
        except:
            json_str = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', json_str)
            json_str = re.sub(r'\s+', ' ', json_str)
            try:
                return json.loads(json_str)
            except:
                pass
    return None
Enter fullscreen mode Exit fullscreen mode

구성 데이터 구조

에이전트 활동 구성

@dataclass
class AgentActivityConfig:
    agent_id: int
    entity_uuid: str
    entity_name: str
    entity_type: str
    activity_level: float = 0.5
    posts_per_hour: float = 1.0
    comments_per_hour: float = 2.0
    active_hours: List[int] = field(default_factory=lambda: list(range(8, 23)))
    response_delay_min: int = 5
    response_delay_max: int = 60
    sentiment_bias: float = 0.0
    stance: str = "neutral"
    influence_weight: float = 1.0
Enter fullscreen mode Exit fullscreen mode

시간 시뮬레이션 구성

@dataclass
class TimeSimulationConfig:
    total_simulation_hours: int = 72
    minutes_per_round: int = 60
    agents_per_hour_min: int = 5
    agents_per_hour_max: int = 20
    peak_hours: List[int] = field(default_factory=lambda: [19, 20, 21, 22])
    peak_activity_multiplier: float = 1.5
    off_peak_hours: List[int] = field(default_factory=lambda: [0, 1, 2, 3, 4, 5])
    off_peak_activity_multiplier: float = 0.05
    morning_hours: List[int] = field(default_factory=lambda: [6, 7, 8])
    morning_activity_multiplier: float = 0.4
    work_hours: List[int] = field(default_factory=lambda: [9, 10, 11, 12, 13, 14, 15, 16, 17, 18])
    work_activity_multiplier: float = 0.7
Enter fullscreen mode Exit fullscreen mode

완전한 시뮬레이션 매개변수

@dataclass
class SimulationParameters:
    simulation_id: str
    project_id: str
    graph_id: str
    simulation_requirement: str
    time_config: TimeSimulationConfig = field(default_factory=TimeSimulationConfig)
    agent_configs: List[AgentActivityConfig] = field(default_factory=list)
    event_config: EventConfig = field(default_factory=EventConfig)
    twitter_config: Optional[PlatformConfig] = None
    reddit_config: Optional[PlatformConfig] = None
    llm_model: str = ""
    llm_base_url: str = ""
    generated_at: str = field(default_factory=lambda: datetime.now().isoformat())
    generation_reasoning: str = ""
    def to_dict(self) -> Dict[str, Any]:
        time_dict = asdict(self.time_config)
        return {
            "simulation_id": self.simulation_id,
            "project_id": self.project_id,
            "graph_id": self.graph_id,
            "simulation_requirement": self.simulation_requirement,
            "time_config": time_dict,
            "agent_configs": [asdict(a) for a in self.agent_configs],
            "event_config": asdict(self.event_config),
            "twitter_config": asdict(self.twitter_config) if self.twitter_config else None,
            "reddit_config": asdict(self.reddit_config) if self.reddit_config else None,
            "llm_model": self.llm_model,
            "llm_base_url": self.llm_base_url,
            "generated_at": self.generated_at,
            "generation_reasoning": self.generation_reasoning,
        }
Enter fullscreen mode Exit fullscreen mode

요약표: 에이전트 유형별 패턴

에이전트 유형 활동 활동 시간 시간당 게시물 시간당 댓글 응답(분) 영향력
University 0.2 9-17 0.1 0.05 60-240 3.0
GovernmentAgency 0.2 9-17 0.1 0.05 60-240 3.0
MediaOutlet 0.5 7-23 0.8 0.3 5-30 2.5
Professor 0.4 8-21 0.3 0.5 15-90 2.0
Student 0.8 8-12, 18-23 0.6 1.5 1-15 0.8
Alumni 0.6 12-13, 19-23 0.4 0.8 5-30 1.0
Person (default) 0.7 9-13, 18-23 0.5 1.2 2-20 1.0

결론

LLM 기반 대규모 시뮬레이션 구성 자동화는 다음과 같은 원칙을 지키면 안정적이고 실용적으로 구현할 수 있습니다.

  1. 단계별 생성: 시간, 이벤트, 에이전트, 플랫폼 순으로 분할 처리
  2. 배치 처리: 15개 단위로 에이전트 구성 생성
  3. JSON 복구: 괄호, 문자열 오류 자동 보정
  4. 규칙 기반 대체: LLM 실패 시 기본값으로 fallback
  5. 유형별 패턴: 에이전트 유형에 따라 활동/응답/영향력 자동 분기
  6. 유효성 검사 및 수정: 생성값 자동 검증 및 보정

Top comments (0)