작동 원리

긴 문서는 실제로 어떻게 쓰이는가

마흔 쪽짜리 보고서는 아주 긴 답변 한 번이 아닙니다. 상태 기계를 가진 백그라운드 작업입니다 — 개요, 질문, 조사, 절 단위 집필. 어느 단계든 죽은 뒤 다시 실행해도 작업이 중복되지 않고 예산도 타지 않으며, 편집의 오프셋은 모델이 고르는 게 아니라 코드가 계산하고, 모든 버전이 남습니다.

평범한 답변은 모델에 보내는 요청 하나입니다. 문서는 아닙니다. 몇 분에서 수십 분을 돌고, 탭을 닫아도 살아남아야 하며, 어떤 요청에도 담기지 않는 마흔 쪽에 걸쳐 일관성을 유지해야 합니다. 그래서 이것은 상태 기계를 가진 백그라운드 작업으로 돌고, 아래의 거의 모든 설계 결정은 한 가지 질문에서 나옵니다 — 이게 중간에 죽으면 어떻게 되는가?

단계들

단계하는 일중간에 죽으면
유형 고르기요청을 당신이 정의한 문서 유형 중 하나에 맞춤다시 실행, 덮어쓰기
개요절들을 만들어 냄행 삽입은 충돌 시 건너뜀
질문쓰기 전에 필요한 것을 한 번 물음기다림 — 당신의 답과 작업 깨우기는 같은 트랜잭션
조사절별로 사실을 라운드 단위로 수집다음 라운드부터 재개
집필한 번에 한 절절을 선점하고, 쓰고, 다음으로
마무리집계, 알림, 내보내기 가능멱등

설계를 가장 잘 설명하는 것은 조사 단계입니다. 이 단계만은 정말로 멱등하게 만들 수 없습니다. 같은 라운드를 두 번 돌리면 검색이 다른 페이지를 돌려줄 수 있고 추출된 문장도 달라질 수 있습니다. 그래서 멱등성 대신 두 가지를 씁니다 — 사실은 정규화 후 (문서, 절, 사실) 기준으로 유일하므로 중복은 두 번째 행으로 들어가지 못하고, 라운드 카운터는 그 라운드의 사실과 같은 트랜잭션에서 증가합니다.

결과는 분명히 말할 가치가 있습니다. 3라운드에서 계속 죽는 작업은 복구 후 4라운드부터 이어집니다. 이것이 없으면 재시작할 때마다 1라운드부터 다시 시작하고, 재시작 횟수에는 상한이 없습니다. 크래시 루프가 예산을 태울 수 없습니다.

왜 한 번에 한 절인가

각 절은 compare-and-set(ready → writing)으로 선점되고, 본문과 버전과 카운터를 한 트랜잭션에서 함께 씁니다. "문서 중간에 서버가 재시작됐다"가 사건이 되지 않는 이유가 이것입니다. 끝난 절은 한 글자도 건드려지지 않고, 진행 중이던 절은 반쯤 두 번 쓰이는 대신 다시 선점됩니다.

동시에 요청을 작게 유지합니다. 한 절은 그 절의 지시, 그 절의 사실, 짧은 맥락으로 쓰입니다 — 문서 전체가 아니라. 이건 들리는 것보다 중요합니다. 자료가 늘어날 때 도구 호출 신뢰도가 어떻게 무너지는지 측정했습니다 — 적은 자료에서 8번 중 7번 성공하던 것이 2/8이 되고, 5,000단어 언저리에서 0/8이 됩니다. 모든 것을 넣은 프롬프트는 더 좋은 절을 만들지 않습니다. 지시를 따르는 대신 자료를 되뇌는 절을 만듭니다.

편집: 오프셋은 코드가 준다

캔버스에서 한 대목을 선택하고 무엇을 바꿀지 말하면, 그 대목만 교체됩니다. 이 메커니즘은 일부러 좁게 만들었습니다.

  1. 클라이언트가 문자 범위 [start, end)를 보냅니다.
  2. 서버가 body[start:end]를 바꿀 조각으로 취합니다.
  3. 모델은 그 조각과 한정된 맥락을 받고 교체 텍스트만 돌려줍니다.
  4. 서버가 이어 붙입니다: body[:start] + 교체 텍스트 + body[end:].

모델에게 범위를 묻는 일은 없고, 모델이 diff를 돌려주는 일도 없습니다. 편집 범위를 내놓으라고 요구받은 모델은 확신이 없을 때 요란하게 실패하지 않습니다 — 완벽히 그럴듯해 보이는 범위를 돌려주고, 당신의 문서에서 문장 하나가 조용히 사라집니다. 범위는 그 텍스트를 렌더링한 블록 분할기에서 나오므로 구조상 정확합니다.

편집하는 동안 그 절이 아래에서 바뀌었다면 — 마침 에이전트가 다시 쓰고 있었다면 — 편집은 덮어쓰기가 아니라 충돌을 돌려줍니다. 어느 쪽을 남길지는 당신이 정합니다. 그건 당신의 권한이고, 대신 정해 버리는 방식에서는 사람들이 며칠 뒤에야 알게 됩니다.

사실, 그리고 출처 없는 숫자의 운명

각 절은 사실 테이블에서 쓰입니다. 모델의 기억에서가 아닙니다. 각 사실은 출처와 함께 저장되므로, 완성된 문서의 어떤 수치든 어떤 페이지나 대목까지 되짚을 수 있습니다.

다시 쓴 뒤에는 결과를 코드가 훑습니다. 수치, 코드, 고유명사 중 인용된 자료에 없는 것은 표시됩니다. 발견되면 그 대목을 한 번 재생성하고, 그래도 남아 있으면 그 대목은 표시된 채로 돌아오고 자동 적용되지 않습니다. 특히 이 점검은 모델에게 스스로 확인시키는 방식이 아닙니다 — "이 대목에 근거가 있느냐"고 물으면, 모순되는 자료를 앞에 두고도 모델은 있다고 답합니다.

버전

절의 모든 변화가 하나의 버전입니다: 첫 초안, 당신의 손질, 요청한 각각의 다시 쓰기, 각각의 복원. 옛 버전 복원은 무언가를 지우는 대신 버전을 더합니다. 그래서 이력은 살아남은 것의 기록이 아니라 실제로 일어난 일의 기록으로 남습니다.

대화에 남는 것

집필은 대화에 세 종류의 메시지를 남기고, 그중 어느 것도 모델이 쓴 것이 아닙니다.

  • 시작 — 제목과 절 수. 요청이 들어온 시점이 아니라 작업이 실제로 토큰을 쓰기 시작한 순간에 올라갑니다.
  • 수정됨 — 어떤 절이 바뀌고 무슨 일이 있었는지. 어떤 턴이 실제로 문서를 바꿨을 때 올라갑니다.
  • 완료 — 절 수, 단어 수, 걸린 시간.

세 가지 모두 숫자는 데이터베이스에서 계산됩니다. 취향의 문제가 아닙니다. 방금 무엇을 했는지 보고하라는 요구를 받은 모델은 자신 있고 구체적이며 때로 사실이 아닌 설명을 만들어 냅니다 — 문서가 한 글자도 바뀌지 않은 턴에서 "그 차트를 문서에 추가했습니다"라고 말하는 것까지 포함해서. 시스템이 무엇을 했는지는 시스템이 말합니다.

약속하기 전에 알아 둘 한계

  • 쓰기 워커는 데이터베이스마다 하나씩 돕니다. 작업은 FOR UPDATE SKIP LOCKED로 선점하므로 선점 의미론 자체는 이미 여러 워커를 견디지만, 현재 배포는 하나만 돌립니다.
  • 취소는 두 반쪽으로 되어 있습니다. 프로세스 안의 반쪽이 현재 단계를 빠르게 멈추고, 작업 행에 남는 플래그가 프로세스보다 오래 산 작업도 멈춥니다.
  • 에이전트에서 공개 소스를 꺼 두었다면 조사 단계는 인터넷에 아예 닿지 않습니다. 성능이 떨어지는 게 아니라 건너뜁니다. 권한은 좁히는 방향으로만 작동합니다.