# 분석 축 만들기 — Affinity Bubble API `https://api.affinitybubble.com` 정해둔 기준으로 **원문 전량을 코딩**한다. 한 행에 여러 축의 값이 동시에 붙고, 값마다 **근거 문장과 근거 등급**이 남는다. 리서처가 코드북을 짜서 손으로 전수 코딩하던 작업이다. **공통 사항(가입·키 발급·크레딧·한도)은 이 문서에 없다.** → https://api.affinitybubble.com/llms.txt 를 먼저 읽는다. **전제: 완성된 버블이 있어야 한다.** 축은 버블 위에 얹으며 **버블 자체는 바뀌지 않는다.** 먼저 https://api.affinitybubble.com/llms/bubbles.txt 로 버블을 만든다. ## 이 문서를 스킬로 만들기 - **순서**: 버블 생성 → `candidates`(축 정하기) → `validate`(표본 시험) → `code`(전수) → `GET` - **모든 POST 에 `Idempotency-Key` 헤더가 필수다.** 없으면 422 - **전수 코딩은 한 번에 1~4축.** 그 이상은 422 - **키**: 스킬 파일에 넣지 말고 별도 파일(chmod 600)에 두고 경로만 참조한다 --- ## 1. 축을 쓸 때와 안 쓸 때 **축은 유형을 만들어주지 않는다.** 행마다 값을 찍을 뿐이다. 축 4~5개에 멀티값이 섞이면 조합이 수십만 가지가 되고, 거기서 "대표 유형 5개"는 나오지 않는다. | 원하는 것 | 쓸 것 | |---|---| | 보고서에 적을 **유형 이름 몇 개** | 버블. 대분류(metaLabel)가 곧 유형 이름이다. `params.lens` 로 관점을 고른다(`topic`·`persona`·`stance`) | | "무엇을 따지고 만족하는가" | [속성 분석](https://api.affinitybubble.com/llms/aspects.txt) | | **기준이 이미 정해져 있고** 그 기준으로 전량을 세야 함 | **분석 축** | | 유형별로 기준값이 어떻게 다른지 | 버블로 유형을 만들고, 축으로 잰 뒤 행 id 로 교차 | 축이 맞는 전형적인 경우는 보고 양식이 밖에서 내려올 때다 — "교통수단별·요구유형별 건수를 내라", "민원을 담당 부서 기준으로 전부 분류하라". ## 2. 네 단계 전량을 코딩한 **뒤에** 기준이 틀린 것을 알면 비용이 이미 나갔다. 그래서 확정 전에 사람이 끼어들 자리를 만들어 두었다. ### 2.1 `POST /v1/bubbles/:id/axes/candidates` — 축 정하기 두 모드다. **`generate`(기본)** — 데이터에서 축 후보 12~15개를 뽑는다. 비동기(`xjob_`). ```bash curl -X POST https://api.affinitybubble.com/v1/bubbles/$ID/axes/candidates \ -H "Authorization: Bearer ab_live_..." \ -H "Idempotency-Key: $(uuidgen)" -H 'Content-Type: application/json' \ -d '{ "decisionContext": { "role": "도시계획과 주무관", "decision": "다음 분기 검토 대상 민원을 고른다", "audience": "과장 보고" } }' # 202 → { "runId": "...", "instanceId": "xjob_...", "statusUrl": "/v1/bubbles/jobs/xjob_..." } ``` **`provided`** — 이미 있는 코드북을 등록만 한다. **LLM 을 부르지 않아 201 동기·무료다.** ```bash -d '{ "decisionContext": {...}, "mode": "provided", "seedAxes": [ ...축 정의... ] }' ``` | Field | Required | Notes | |---|---|---| | `decisionContext` | ✅ | `role`·`decision`(필수) + `audience`(선택). **무슨 결정을 하려는지가 축의 입력이다** — 이게 없으면 그럴듯하지만 결정에 못 쓰는 축이 나온다 | | `mode` | — | `generate`(기본) · `provided` | | `seedAxes` | `provided`면 ✅ | 최대 15개. 형태는 §3 | | `texts` | — | 원본 텍스트(입력 순서). 있으면 값 어휘를 원문에서 뽑는다 | | `metadataColumns` | — | 별점·지역처럼 이미 정형인 열을 축으로 쓸 때 | | `sampleSize` | — | 후보 생성에 읽을 표본 수(10~200) | ### 2.2 `POST /v1/bubbles/:id/axes/:runId/validate` — 표본 시험 코딩 **40건만 먼저 코딩해서 이 축이 쓸 만한지 본다. 전수는 시작하지 않는다.** ```bash -d '{ "definitionsRevision": 1, "sampleSize": 40 }' # 둘 다 선택 ``` 나오는 것: 축마다 `coverage`(값이 붙은 비율) · `discrimination`(값이 갈리는가) · `quoteRate`(근거를 인용으로 댄 비율) · `noneRate` · `redundancy`(축끼리 겹치는가) · `findings`(경고 코드). 경고가 붙는 기준: | 코드 | 언제 | |---|---| | `low_coverage` | 표본 coverage 20% 미만 | | `low_discrimination` | 한 값 집합이 95% 넘게 차지 — 사실상 한 값이다 | | `possible_redundancy` | 다른 축과 NMI 0.9 이상. **높다고 자동 병합하지 않는다** | | `all_none` | 전부 근거 없음 | | `small_sample` | 표본이 얇아 수치를 믿기 어렵다 | 여기서 축 정의를 고치고 다시 검증하는 것이 정상 경로다. ### 2.3 `POST /v1/bubbles/:id/axes/:runId/code` — 전수 코딩 ```bash -d '{ "axisIds": ["mode", "ask", "owner"], "validationRevision": 2 }' ``` - **`axisIds` 는 1~4개다.** 0개나 5개 이상이면 422. 축이 더 필요하면 run 을 나눈다. - 검증을 먼저 돌렸어야 한다. 없으면 422(`validationRevision` 을 직접 주면 그것을 쓴다). - 검증에서 채택 가능한 축이 하나도 없으면 422 `no_viable_axes`. **4축을 한 배치에 묶어 코딩한다.** 축별로 따로 돌리면 같은 원문을 네 번 보내야 해서 입력 토큰 3.1배·총비용 1.8배인데 정확도가 낫다는 증거가 없다(120건×4축×2회 실측). **다만 "같은 결과"는 아니다** — §5 를 본다. ### 2.4 `GET` · `PATCH /v1/bubbles/:id/axes/:runId` `GET` 은 단계별 산출물을 돌려준다 — 축 정의, 탈락한 후보와 **탈락 이유**, 검증 수치, 행별 셀과 근거, provenance. 기본은 최신 완료 리비전이다. `PATCH` 는 사람이 고친다(동기 200). **원본을 지우지 않고 새 리비전으로 쌓인다.** 정의를 고치면 그 아래 검증이 낡은 것으로 표시되고, 결과 셀을 고치면 원본을 보존한 새 결과가 생긴다. ## 3. 축 정의의 형태 (`seedAxes[]`) ```jsonc { "name": "요구 유형", "definition": "제안이 실제로 무엇을 해달라는가.", "boundary": "없던 것을 만들라 = 신설·증설. 있는 것을 고치라 = 개선. 남의 행위를 막으라 = 규제·단속.", "decisionUse": "요구 유형별로 다음 분기 검토 우선순위를 정한다.", "valueType": "nominal", // nominal | ordinal "cardinality": "multi", // single | multi "knowledgePolicy": "text_only", // text_only | external_allowed "chunkAggregation": "union", // union | whole_text_required "evidenceRule": "요구를 말한 구절을 인용한다.", "nullRule": "요구가 없으면 비운다.", "values": [ { "id": "new_build", "name": "신설·증설", "definition": "없던 것을 만들거나 수를 늘리라는 요구.", "include": ["신설", "증설", "증차"], "exclude": ["기존 것을 고치라는 요구"] } ] } ``` **`include`/`exclude` 가 제일 중요하다.** 실측에서 묶음·분리가 갈린 20건 중 13건은 두 조건이 **글자까지 같은 구절을 인용하고도 다른 값을 붙였다.** 읽기의 차이가 아니라 "이 경우는 어느 값이냐"가 정의에 안 적혀 있던 것이다. 결과가 흔들리면 손볼 곳은 여기다. **`ordinal` 이어도 평균·점수를 만들지 않는다.** 순서가 있다는 표시일 뿐이다. **"없음"을 값으로 두지 않는다.** 값 목록에 "해당 없음"을 넣으면 모델이 전부 채워서 `none` 이 0% 가 되고, 근거 없는 행이 집계 모수에서 빠지는 장치가 무력화된다. 근거가 없으면 비게 두는 것이 옳다. ## 4. 근거 등급 **행·축·값마다 따로** 붙는다. 축 전체에 단일 등급을 덮어쓰지 않는다. | 등급 | 뜻 | 확인가능성 | |---|---|---| | `quoted` | 원문에 그대로 있다 | L1 명시 | | `inferred` | 글에서 읽어냈다 | L2 본문 함축 / L3 외부지식(`externalAssumptions` 필수) | | `none` | 댈 근거가 없다 | L4 판정 불가 | **`none` 은 값 집계의 모수에서 빠진다.** 모르는 것을 "기타"로 밀어 넣어 비율을 만들지 않는다는 뜻이다. 인용은 원문에서 위치(`start`·`end`)까지 검증하며, 원문에 없는 인용은 버리고 `invalidQuoteRate` 에 남긴다. `knowledgePolicy: 'text_only'` 인 축은 외부지식 판정을 허용하지 않는다. 관할·상식으로 추정해야 하는 축(예: 실행 주체)만 `external_allowed` 로 연다. ## 5. 알아둘 한계 - **묶음 코딩은 "같은 정확도"가 아니다.** 값 경계가 헐거운 축에서는 4축 묶음과 축별 분리의 값 분포 1·2위가 뒤집힌 사례가 있다. 순위가 결정을 바꾸는 축이라면 정의의 `include`/ `exclude` 를 먼저 조인다. - **어느 쪽이 맞는지는 아직 재지 않았다.** 위 실측은 두 조건이 얼마나 **다른가**를 쟀을 뿐 어느 쪽이 **옳은가**는 사람이 매긴 정답 표본 없이 답이 없다. - **축 이름과 정의를 LLM 이 만든 경우(`generate`) 도메인 표준 분류가 아니다.** 검증 단계에서 반드시 읽고 고친다. - **긴 원문은 청크로 나뉜다.** `chunkAggregation` 이 `union` 이면 청크별 값의 합집합, `whole_text_required` 면 전문을 한 번에 봐야 하는 축이다(입장·태도처럼). 토큰 예산을 넘겨 원문이 잘리면 응답에 `truncated` 로 표시된다 — 조용히 넘어가지 않는다. - **MVP 범위:** 완료된 버블 하나, 후보 최대 15개, 전수 1~4축. ## 6. 크레딧 | 작업 | 크레딧 | |---|---| | `candidates` (`generate`) | 1 | | `candidates` (`provided`) | 무료 | | `validate` | 2 | | `code` | 텍스트 수 기준 — 500건까지 15, 이후 100건마다 +3 | | `GET` · `PATCH` | 무료 | ## 7. 에러 | status | type | 뜻 | |---|---|---| | 422 | `invalid_request` | POST 에 `Idempotency-Key` 헤더가 없다 | | 404 | `not_found` | 버블·run·리비전이 없다 | | 403 | `forbidden` | 다른 키가 소유한 버블 | | 422 | `invalid_request` | `axisIds` 가 1~4개가 아니거나, 검증을 안 돌렸다 | | 422 | `no_viable_axes` | 검증에서 채택 가능한 축이 없다 — 결정 맥락·정의를 손본다 | | 402 | `insufficient_credits` | 크레딧 부족 |