구현 반영됨(ai#5·#6,
app/). 이 문서는 계약 명세이며 구현이 이를 따른다. 리포트: implements/2026-07-23-fastapi-implementation.md. 공용 계약은 Team-PinLog/docs의static/05_AI_설계.md를 따릅니다.
근거 계약: static/05_AI_설계.md §6.1 상태 컬럼, §7.4 부분 재사용, §10.2 부분 실패
ai.context_ai_state가 두 개의 독립 status를 가지므로 다음 상태가 정상적으로 존재합니다.
embedding_status = COMPLETED
keyword_status = PENDING
Embedding은 성공했고 LLM 판정만 실패한 경우입니다. Spring 재스캔이 이 Context를 다시 FastAPI로 보낼 때, Embedding API를 다시 호출하지 않고 Keyword 단계부터 재개합니다.
Embedding 호출은 이 파이프라인에서 가장 비싼 외부 호출 중 하나이고, 같은 본문에 대해 같은 벡터를 다시 만드는 일이므로 재호출은 순수한 낭비입니다.
Embedding 재사용은 두 조건을 모두 만족할 때만 허용합니다.
1. state.embedding_status == 'COMPLETED' (그리고 Embedding 행이 존재)
2. embedding.embedding_profile == 현재 Embedding Profile
조회는 한 번의 Query로 수행합니다.
SELECT e.embedding,
e.embedding_profile AS emb_profile,
s.embedding_status,
s.keyword_status
FROM ai.context_ai_state s
LEFT JOIN ai.context_embedding e ON e.context_id = s.context_id
WHERE s.context_id = :context_id;조건별 의미:
| 조건 | 없으면 생기는 일 |
|---|---|
| 1 | 저장 중 중단되어 불완전하거나 아직 없는 행을 재사용 |
| 2 | 차원·거리 기준이 다른 벡터를 Preset과 비교 |
Context 본문 일치를 확인하는 조건은 없습니다. Context가 불변이므로
같은 context_id의 Embedding은 정의상 같은 본문의 산출물입니다(계약 §4.2).
State와 Embedding 행 사이에 버전이 어긋나는 중간 상태 자체가 존재하지 않습니다.
수정된 Context는 새 context_id이므로 구 Context의 Embedding을 재사용하지 않습니다.
재사용 판정은 context_id 단위 조회이고 신 Context에는 아직 Embedding 행이 없으므로,
조건 1에서 자연히 걸러집니다. 구 Context의 Embedding을 신 Context로 승계하는 경로를
별도로 만들지 않습니다(계약 §4.2 승계 금지).
2가 실패하면 재사용이 아니라 재생성 대상입니다. Profile 불일치 시의 동작은 model-profile.md를 따릅니다.
is_deleted는 재사용 판정에 사용하지 않습니다. 삭제·수정된 Context는 status가 CANCELLED이므로
조건 1에서 이미 걸러지며, is_deleted는 검색 단계의 보조 방어선입니다.
요청 하나를 받았을 때 단계별로 독립 판단합니다.
flowchart TD
A[사전 검사 통과] --> B{embedding 재사용 2조건}
B -->|만족| R[메모리에 벡터 적재, Embedding 단계 건너뜀]
B -->|불만족| C{embedding 시작 가능?}
C -->|아니오| S[Embedding 단계 중단]
C -->|예| D[PROCESSING 전환 → API 호출 → 저장]
D --> R
R --> E{keyword 시작 가능?}
E -->|아니오| T[Keyword 단계 중단]
E -->|예| F[PROCESSING 전환 → 후보 검색 → LLM → 저장]
"시작 가능"의 판정은 조건부 UPDATE 한 번이며, 그 허용 집합은
PENDING 또는 **만료된 stale PROCESSING**입니다(state-machine.md §3.1).
구현상의 결론:
-
Embedding 단계를 건너뛰었다고 해서 Keyword 단계도 건너뛰지 않습니다. 두 단계의 진행 여부는 각자의 status가 결정합니다.
-
Embedding 단계가 중단되었더라도 Keyword 단계는 시도합니다.
embedding_status가 COMPLETED여서 중단된 경우가 정확히 재개 경로입니다. -
Keyword 단계에 필요한 벡터는 재사용 판정에서 이미 읽어 둔 값(
carried)을 우선 씁니다. 다만 그 값이 없으면context_embedding에서 DB로 재조회합니다(keyword_service._resolve_vector).판단 변경(2-worker 경합 방어). 원래 계약은 *"후보 검색을 위해 Embedding을 다시 조회하지 않는다"*였다. 이 문장은 한 요청이 재사용 판정에서 벡터를 항상 손에 쥔다는 단일 워커 전제 위에 있었다. 실제 구현은 다른 워커가 임베딩을 완료해 이 요청의 재사용 판정에는
carried가None으로 오는 경합 경로를 방어하려 fallback 재조회를 넣었다 (_resolve_vector가carried is None일 때context_embedding_repo.load_vector호출). 즉 코드가 맞고 이 문장이 낡았다. 재조회는 정상 경합 대응이지 "벡터 재사용" 원칙의 위반이 아니다. (출처: 종료된 이전 작업 세션(S1)의 기록 복원으로 경위를 확인했고,keyword_service._resolve_vector:108-115코드에서 직접 확인했다)
embedding_status |
keyword_status |
동작 |
|---|---|---|
| COMPLETED | PENDING | 재개. 벡터 재사용, Keyword만 수행 |
| PENDING | PENDING | 전체 수행 |
| COMPLETED | FAILED | 아무것도 하지 않음. FAILED는 재스캔 대상이 아니며 PROCESSING으로 직접 전이 불가 |
| FAILED | PENDING | Keyword만 시도. 다만 재사용 2조건을 만족하는 Embedding이 없어 판정에 쓸 벡터가 없으므로, Keyword 단계도 시작하지 않음 |
| COMPLETED | COMPLETED | 할 일 없음 |
| COMPLETED | PROCESSING (만료) | 재개. 벡터 재사용, stale Keyword 작업 재선점 |
| CANCELLED | CANCELLED | 처리·저장 대상 아님. 삭제되었거나 수정으로 대체된 구 Context |
FAILED / PENDING 조합에서 Embedding을 새로 만들어 Keyword를 진행하지 않는 이유는,
그것이 FAILED → PROCESSING 전이를 우회하는 것과 같기 때문입니다.
FAILED는 진짜 종결 상태이며, 같은 context_id를 다시 처리하는 경로는 존재하지 않습니다
(계약 §6.3). 사용자가 본문을 고치면 그것은 새 context_id의 새 State로 처리되며,
그 처리는 이 Context의 재개가 아닙니다.
Keyword 저장 조건이 embedding_status = COMPLETED를 함께 요구하므로(계약 §6.6),
FAILED / PENDING 조합은 설령 시작되더라도 저장 단계에서 다시 걸립니다.
재개로 Keyword를 다시 판정하면 기존 ai.context_keyword 행이 남아 있을 수 있습니다.
저장은 항상 해당 Context의 기존 행 삭제 후 삽입으로 수행합니다
(keyword-preset.md §5).
Embedding은 반대로 UPSERT입니다. Context당 한 행이며 is_deleted를 건드리지 않습니다.
검증 시나리오 7(Embedding 성공, Keyword 실패 → Embedding 재호출 없이 Keyword만 재개)이 이 문서의 주 검증 대상입니다. 테스트는 Embedding Client에 대한 호출 횟수를 0으로 단언합니다. 상세는 integration-tests.md.