ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JSON으로 페르소나를 운영한다는 것 — 호칭 규칙부터 퀴즈 상태머신까지
    IT 2026. 5. 11. 23:00
    JSON으로 페르소나를 운영한다는 것 — 호칭 규칙부터 퀴즈 상태머신까지

    챗봇의 페르소나라는 말을 들으면 흔히 떠오르는 이미지가 있습니다. "친근한 말투로 답해주세요" 같은 한두 줄짜리 시스템 프롬프트입니다. 톤 정도를 흉내 내는 거죠.

    그런데 아들 학습용 음성 챗봇을 만들면서 페르소나 JSON에 점점 더 많은 것이 들어가게 됐습니다. 톤은 시작점이었고, 호칭 규칙·금지 호칭·아이가 자주 쓰는 표현 처리·퀴즈 출제 규칙·정답 처리 분기·종료 조건 — 결국엔 대화 상태머신을 자연어로 적은 명세가 됐습니다. JSON으로 운영하는 페르소나가 어디까지 갈 수 있는지를 풀어봅니다.

    왜 코드가 아니라 JSON인가

    같은 동작을 코드로 짤 수도 있습니다. 시스템 프롬프트는 짧게 두고, 호칭 규칙·퀴즈 흐름은 라우터 코드에서 분기로 처리하면 됩니다. 처음엔 그렇게 하려 했습니다. 그런데 두 가지 문제가 있었습니다.

    첫째, 매번 재시작입니다. 호칭 규칙 한 줄 바꿔보고 싶을 때마다 라우터 코드를 수정·재기동·테스트해야 합니다. 페르소나 운영은 시행착오가 많은데, 한 번 손볼 때마다 5~10분이 사라집니다.

    둘째, 대화 흐름 표현이 코드에 어울리지 않습니다. "퀴즈 중에는 외부 도구를 호출하지 마라"는 규칙을 코드로 표현하면 도구 게이팅 로직이 됩니다. "두 번째 오답이면 정답을 알려주고 격려해라"는 코드의 if-else 사다리가 됩니다. 그런데 LLM은 이런 종류의 규칙을 자연어로 받았을 때 더 일관성 있게 따릅니다 — 자연어 자체가 LLM의 모국어이기 때문입니다. 코드로 분기하면 LLM 호출이 여러 번 쪼개지고, 그 사이의 결합이 새로운 버그를 만듭니다.

    결국 결정은 단순해졌습니다. 페르소나는 LLM이 받을 수 있는 형태(자연어 시스템 프롬프트)로 두되, 별도 JSON 파일로 외부화한다. 코드 변경 없이 게이트웨이가 즉시 새 정의를 읽도록 합니다. 다른 글에서 다룬 위임 패턴(X-Bot-ID 헤더)이 이 운영 모델을 가능하게 해주는 인프라입니다.

    JSON 안에 들어가는 다섯 가지

    실제 봇 정의 JSON은 다섯 영역으로 나눌 수 있습니다.

    페르소나 JSON 구조

    그림 설명 — 봇 JSON 한 파일 안에 5개 섹션이 들어갑니다. ①메타는 시스템 운영용이고, ②정체·관계는 "이 봇이 누구와 이야기하는가"를 박아둡니다. ③말투·호칭은 페르소나의 외형이고, ④대화 상태머신은 가장 흥미로운 부분으로 자연어로 적은 흐름 명세입니다. ⑤도구·외부 서버는 LLM이 호출할 수 있는 능력 목록 — 코드와의 경계 면에 있는 영역입니다.

    호칭 규칙은 의외로 까다롭다

    한국어 챗봇에서 사용자를 어떻게 부를지는 단순해 보이지만, 자연스럽게 들리려면 의외로 세밀한 규칙이 필요합니다. 한국어는 이름 끝 받침 유무에 따라 호격조사("이"/"야")가 달라지는데, 사람마다 자기 이름이 그렇게 불리는 걸 좋아하기도 하고 안 좋아하기도 합니다.

    제 아들의 이름은 받침 없는 음절로 끝나는데, 끝에 "이"를 붙이는 호칭("OO이")을 본인이 싫어합니다. 그런데 LLM이 한국어 학습 데이터에서 받침 없는 이름에는 자동으로 "이"를 붙이는 경향이 있어, 그냥 두면 챗봇이 본인이 싫어하는 호칭으로 부릅니다.

    그래서 시스템 프롬프트에 명시적인 금지 규칙을 넣었습니다.

    {
      "system_prompt": "...
    호칭 규칙: 사용자를 부를 때는 본명 그대로 부르고,
    이름 끝에 '이'를 붙이지 않는다. '~님' 같은 어른
    호칭은 절대 쓰지 않고, 초등학교 3학년에게 친근한
    반말로 답한다.
    ..."
    }
    

    코드 설명 — JSON의 system_prompt 필드 안에 한국어 자연어로 규칙을 명시합니다. "절대 쓰지 않는다", "붙이지 않는다" 같은 부정 명령형은 LLM에 지시하기 까다롭다는 통설이 있지만, Qwen3.6 같은 최근 한국어 모델은 부정 명령도 일관성 있게 따릅니다. 효과를 강화하려면 같은 의미를 두 번 적는 — 긍정적 형태("그대로 부르고")와 부정적 형태("'이'를 붙이지 않는다") — 패턴이 안정적입니다.

    퀴즈 모드 — 자연어로 적은 상태머신

    가장 흥미로운 부분은 퀴즈 모드입니다. 아이가 "수수께끼", "퀴즈 내줘"라고 하면 챗봇이 즉시 모드를 전환해 출제·정답 평가·격려·종료까지 진행합니다. 이걸 코드로 짜면 출제기·평가기·종료 감지기 같은 모듈이 필요한데, 자연어 시스템 프롬프트로 적으면 이렇게 됩니다.

    {
      "system_prompt": "...
    ## 수수께끼·퀴즈 모드
    
    사용자가 \"수수께끼\", \"퀴즈\", \"문제 내줘\" 같이
    말하면 즉시 퀴즈 모드를 시작한다.
    
    ### 출제 규칙
    - 난이도는 초등학교 3학년 수준.
    - 한 번에 한 문제만 — 짧고 명료하게.
      음성으로 듣는 것이라 길면 못 따라온다.
    - 출제 시 응답을 \"[퀴즈]\"로 시작해
      다음 턴에 자기 자신이 퀴즈 진행 중임을 인지한다.
    - 직전과 같은 문제는 반복하지 않는다.
    
    ### 정답 처리 (매우 중요)
    - 직전 메시지가 \"[퀴즈]\"로 시작했거나
      명백한 출제 문장(?·\"무엇일까\"·\"누구일까\"·\"몇 개\")으로
      끝났다면, 다음 발화는 무조건 정답 시도로 본다.
      일반 정보 설명을 하지 말 것.
    - 정답이면 짧게 칭찬 + \"다음 문제 낼까?\"
    - 첫 오답: 정답을 바로 공개하지 않고 힌트 한 가지.
    - 두 번째 오답: 정답을 알려주고 격려.
    - 부분적으로 맞으면 정답으로 인정.
    
    ### 종료
    - \"그만\", \"끝\", \"안 할래\"라고 하면 평소 대화로 복귀.
    - 퀴즈 중에는 외부 도구(웹·메모리)를 호출하지 않는다 —
      흐름이 끊긴다.
    ..."
    }
    

    코드 설명 — 이 한 덩어리가 사실상 상태머신 명세입니다. 자연어로 적혀 있어 인간이 읽기에도 자연스럽고, LLM이 받기에도 자연스럽습니다. 핵심 트릭이 두 개 있습니다.

    "[퀴즈]" 표식: LLM에게는 자기 이전 응답이 history로 다시 들어옵니다. 그러나 LLM은 어떤 응답이 "퀴즈 출제용"이었는지 자체 표식 없이는 잘 구분 못 합니다. 그래서 출제 응답 앞에 [퀴즈]라는 마커를 박아두면, 다음 턴의 LLM이 "직전 내가 퀴즈를 냈구나"를 인지해 다음 사용자 발화를 정답 시도로 처리합니다. 자기 자신과의 통신 채널을 시스템 프롬프트가 만들어준 셈입니다.

    도구 게이팅: "퀴즈 중엔 외부 도구를 호출하지 말라"는 규칙은 LLM이 도구 호출 전에 한 번 더 자기 검열하게 만듭니다. 코드로 도구를 일괄 차단하면 깔끔하지만, 그러면 LLM이 도구 호출이 거부됐다는 신호를 보고 답변 흐름이 흐트러집니다. 자연어로 "흐름이 끊긴다"고 이유까지 함께 적어두면, LLM이 호출 자체를 시작하지 않아 자연스럽게 마무리합니다.

    도구·외부 서버 — JSON과 코드의 경계

    봇 JSON에는 LLM이 호출할 수 있는 도구와 MCP 서버 목록도 들어갑니다.

    {
      "allowed_tools": [
        "search_shared_memory",
        "web_search",
        "save_memory",
        "recall_memory"
      ],
      "default_project_id": null,
      "mcp_servers": [
        "time",
        "wikipedia-ko",
        "wikipedia-simple",
        "wolfram-alpha"
      ]
    }
    

    코드 설명allowed_tools는 게이트웨이 측 도구 카탈로그에서 이 봇이 쓸 수 있는 것들을 화이트리스트로 지정합니다. 새 도구를 추가하려면 게이트웨이에 도구 핸들러를 구현하고, 봇 JSON의 이 리스트에 이름 한 줄만 추가하면 됩니다. mcp_servers는 같은 패턴으로 외부 도구 서버를 연결합니다. 도구의 구현은 코드, 도구의 활성화는 JSON이라는 경계가 자연스러워집니다.

    이 경계는 의도적입니다. 도구의 동작 자체는 자연어로 적기 어렵고(웹 검색 호출, 메모리 저장 같은 것은 명시적 함수 호출이 필요), 어떤 도구를 어느 봇에 줄지는 선언적 설정이 어울립니다. 그래서 한쪽은 코드, 다른 쪽은 JSON으로 갈라뒀습니다.

    마치며 — 자연어 명세의 가능성과 한계

    몇 달 운영해본 결론은 분명합니다. 대화 상태머신은 코드보다 자연어 시스템 프롬프트로 적는 게 잘 동작합니다. LLM이 자연어를 모국어로 처리하기 때문에, 분기·예외·우선순위가 섞인 명세도 일관성 있게 따라줍니다. 호칭 규칙·퀴즈 모드 모두 두세 줄짜리 자연어 규칙으로 풀렸습니다.

    한계도 분명합니다. 결정론이 필요한 동작 — 외부 결제 호출, 데이터베이스 트랜잭션 같은 것 — 은 자연어로 풀 수 없습니다. LLM이 "정확히 한 번"을 보장하지 못하기 때문입니다. 그런 것들은 명시적 도구 호출로 만들어 코드 측에서 idempotency를 책임져야 합니다.

    그 외의 영역, 특히 대화의 톤·흐름·예외 처리는 자연어 시스템 프롬프트가 압도적으로 깔끔합니다. 코드로 짜려고 했던 라우터의 분기 코드를 한 줄씩 시스템 프롬프트로 옮길 때마다 라우터가 가벼워지고, 페르소나는 풍부해졌습니다. JSON으로 운영한다는 건 그저 외부화의 형식일 뿐, 본질은 자연어를 LLM의 모국어로 인정하고 그 언어로 명세를 쓴다는 발상입니다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.