Skip to content

refactor: CEO 지시 분기를 단일 지시점으로 통합하고 부정 판정 결함 5건 수정 - #416

Merged
yellow-jam merged 11 commits into
mainfrom
refactor/ceo-request-classifier
Aug 31, 2026
Merged

refactor: CEO 지시 분기를 단일 지시점으로 통합하고 부정 판정 결함 5건 수정#416
yellow-jam merged 11 commits into
mainfrom
refactor/ceo-request-classifier

Conversation

@yellow-jam

@yellow-jam yellow-jam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

문제 파악

CEO 지시가 어느 레인으로 가는지 정하는 로직이 6개 지점에 흩어져 있었다. 각자는 맞았지만 "어떤 순서로 무엇을 검사하는지"가 한 파일에도 한 함수에도 적혀 있지 않았다. 그 결과 부정 가드가 즉시 주문 레인에만 걸려 있는 것을 아무도 못 봤고, "이평 깨지면 매도하지 마"가 조건주문 카드를 만들었다.

이 PR은 판정 방식을 통일하지 않는다. 주문 판정은 결정론으로 남고(개발원칙 4, ADR-0007), 라우터 결과는 verify_primary_route가 질의만으로 재현 검증할 수 있어야 하며, LLM 플래너는 그 둘 중 어느 것도 대체하지 않는다. 합친 것은 순서다.

작업 내용

구조

  • orchestration/ceo_request_classifier.py 신설 — 번호 붙은 Stage 0~7 파이프라인 단일 지시점
  • orchestration/query_lexicon.py 신설 — 세 곳에 흩어져 어휘가 서로 달랐던 부정 판정을 합집합으로 통합
  • apps/api/ceo.py의 순차 if 체인 6단 제거 → classify_ceo_request() 호출 1회
  • ceo_mirror_api·accounting 별칭 경로도 같은 지시점 경유
  • 기존 공개 함수(build_deterministic_bff_plan·build_ceo_task_plan·verify_primary_route)는 삭제하지 않고 유지

고친 결함

# 증상 수정
1 조건·복합·연계 레인에 부정 가드 없음 4개 레인 대칭 적용. 부정이 지배하는 구간만 보므로 정상 조건주문은 통과
2 explicit_non_execution매수/매도 누락 순수 부정 문장이 binding으로 남아 LLM 플래너로 가던 오분류 해소
3 부정으로 부서를 제거할 수 없음 기본값 선적재 후 add만 하던 구조에 배제 단계 추가
4 부정 어휘가 하지 마 계열뿐 건드리지 말고·빼고·제외하고·말고 인식
5 한 음절 부정의 양방향 오탐 동안이 트레일링 조건주문을 막던 오탐 제거 + 안하고·못하게·안되게 미탐 차단

결함 5는 main 병합 과정에서 발견됐다. main이 새로 추가한 트레일링 문법("최대 5거래일 동안 추적")이 제 가드에 걸렸고, 반대로 띄어쓰기를 생략한 부정은 새고 있었다.

main 통합

  • operational_status를 명시 레인으로 승격 — main이 추가한 이 레인은 부서 primary가 0개인데 department_analysis에 섞여 있었다. 이름이 없으면 다음 사람이 또 호출부에 플래그를 만든다
  • ceo.pydeterministic_operational_status를 플랜 마커 재검사 대신 레인 판정에서 직접 도출
  • 바스켓 2종·트레일링·OCO 등 main 신규 문법을 골든에 편입

검증

항목 결과
레인 판정 (main 순차 체인 vs 분류기, 같은 트리 272건) 불일치 2건 — HR E2E 문장 조각 1건, 테스트 실패 메시지 문자열 1건. 실제 질의 아님
플랜 출력 (main vs 이 브랜치, 61건) 차이 2건, 둘 다 의도한 부서 배제 수정
플랜 직렬화 키 집합 / routing_basis 전건 동일experience_bank·d5_improvement_pipeline·ceo_kanban_read가 읽는 값 불변
부정 형태 조합 260건 (단형×띄어쓰기×어미×동사, 장형, 배제) 미탐 0건
정상 주문/유사 낱말 오탐 검사 오탐 0건
전체 스위트 52 failed / 2638 passed — 실패 목록이 main 기준선과 완전 일치
verify_primary_route department_analysis 레인 전체에 대해 여전히 유효 (테스트로 고정)

52건 실패와 49건 collection error는 전부 main에도 존재한다(Windows에 fcntl 없음).

리뷰 포인트

  • orchestration/query_lexicon.py — 부정 판정 함수가 이제 여기 한 곳에만 있다. 인라인 정규식 0건
  • orchestration/ceo_request_classifier.py Stage 2 — 부정 가드가 4개 레인에 대칭으로 걸리는 지점
  • tests/orchestration/test_ceo_request_classifier_golden.py — 골든 케이스 표. expectedFailure 3건은 담당자 결정이 필요한 미결 항목(코멘트 참조)

🤖 Generated with Claude Code

배포

docker compose up -d --build --no-deps --force-recreate \
  portfolio-bff ceo-kanban-supervisor

yellow-jam and others added 11 commits August 31, 2026 09:58
- 흩어진 6개 분기 지점의 합산 레인 판정을 질의 문자열 단위로 고정
- 현행유지 15건·목적지 미결정 3건을 회귀 기준으로 명시
- 결함 12건을 expectedFailure로 표기해 조용한 유지·변경 차단
- 주문 레인 4종(즉시·조건·복합·연계) 전부에 부정 문장 케이스 배치

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 세 곳에 흩어진 부정 어휘(주문 레인·부서 라우터·workflow_mode)를 합집합으로 통합
- 기존 세 판정은 폭을 그대로 둔 얇은 래퍼로 유지, 어느 하나도 좁히지 않음
- 배제 표현(건드리지 말고·빼고·제외하고·말고)을 별도 상수로 추가, 적용은 후속 커밋
- 부정 지배 구간 추출기 negated_spans·dominant_negated_keys 신설
- 의도 어휘·binding 어휘·비구속 문구 사전을 같은 모듈로 이동
- 골든 결과 불변, 전체 스위트 실패 수 기준선과 동일(49건, 전부 사전 존재)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 6개 분기 지점의 검사 순서를 번호 붙은 Stage 0~7 파이프라인으로 명문화
- CeoRouteDecision에 lane·workflow_mode·routing_basis·reason_codes·order_plan 반환
- 내부는 기존 결정론 함수를 그대로 호출, 판정 정책 중복 구현 없음
- LLM 플래너는 호출하지 않고 lane="llm_planner_required" 반환만 담당
- 골든 결과 이관 전과 동일(통과 3·xfail 12·서브테스트 18)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- apps/api/ceo.py 순차 if 체인 6단 제거, classify_ceo_request 호출 1회로 대체
- 주문 문법 판정 import(조건·즉시·연계) 및 infer_workflow_mode 직접 호출 제거
- ceo_mirror_api·accounting 별칭 경로가 같은 지시점에서 플랜 수신
- 미러 라우팅 로그에 lane 필드 추가
- 골든 결과·전체 스위트 실패 목록 기준선과 동일

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 조건·복합·연계 레인에 즉시 주문과 대칭인 부정 가드 적용
- 부정 지배 구간 안의 주문 행위만 차단해 정상 조건주문은 통과 유지
- explicit_non_execution 행위 어휘에 매수·매도 추가, 순수 부정 문장의 binding 오분류 해소
- "이평 깨지면 매도하지 마"가 "손실 나도 매도하지 마"와 같은 레인으로 수렴
- 결함 표기 7건 해제, 종목 부정 조건주문 통과 회귀 테스트 추가

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 건드리지 말고·빼고·제외하고·말고 계열을 부서 선택 단계의 부정으로 인정
- 되묻기 게이트는 좁은 어휘 유지 - 금지도 대상 지목이므로 재질문으로 떨어뜨리지 않음
- "회계쪽은 건드리지 말고 리서치만 해줘"에서 accounting 추가 제거
- 결함 표기 1건 해제, 되묻기 회귀 방지 테스트 추가

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 기본값 선적재 후 add만 하던 구조에 배제 단계 추가
- 부정이 직접 지배하는 어휘만 제거 대상, 조건절 어휘는 보존
- QA 어휘는 배제 근거가 될 수 없음(응답 primary가 아님), ceo는 제거 불가
- 응답 부서가 전부 사라지는 배제는 기본값 유지로 fail-closed
- excluded_departments는 CeoRouteDecision에만 노출, 플랜 직렬화 모양 불변
- 결함 표기 1건 해제, 조건절 보존·전량 배제 회귀 테스트 추가

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- _is_prohibited_safety_term의 인라인 부정 어휘를 query_lexicon으로 이동
- 부정 판정 함수가 query_lexicon 한 곳에만 존재하도록 정리
- department_analysis 레인 전체가 verify_primary_route로 재현 검증됨을 고정

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- ceo_query_routing 충돌 해소: main의 is_operational_status_query 채택,
  중복 재도입된 인라인 _QUERY_INTENT_TERMS는 query_lexicon 별칭으로 정리
- ceo.py·user_order_language는 자동 병합(분기 체인 구간은 main 변경 없음)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 한 음절 부정 표지(안·못)에 어절 경계 요구, "5거래일 동안 추적"의
  `동안`이 부정으로 잡혀 트레일링 조건주문이 막히던 오탐 제거
- operational_status를 명시 레인으로 승격, 부서 primary 0개인 조회가
  department_analysis에 섞이지 않도록 분리
- ceo.py의 deterministic_operational_status를 레인 판정에서 직접 도출,
  플랜 마커 재검사 제거
- 골든에 바스켓·트레일링·운영상태 케이스와 어절 경계 회귀 테스트 추가

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 안·못 뒤에 공백을 요구하던 조건 제거, 용언 어간(하·되·된·할·함·해·했·사·살·팔)까지 인식
- "주문 안하고"·"매도 못하게"·"체결 안되게"가 조건주문 카드로 새던 경로 차단
- 앞 한글 경계는 유지해 안전·안정·안내·안건·불안·잘못은 계속 통과
- 띄어쓰기 변형 10건·유사 낱말 8건 회귀 테스트 추가

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@yellow-jam

Copy link
Copy Markdown
Collaborator Author

고려사항과 결정

리뷰 전에 읽어야 할 판단 근거. 코드만으로는 안 보이는 것들이다.


1. 사전이냐 형태소 분석기냐 — 사전 유지

주문 게이트에서 LLM/신경망 판정은 효율이 아니라 권한 문제라 후보가 아니다. 개발원칙 4와 ADR-0007이 "주문 후보는 결정론을 통과"를 요구하고, verify_primary_route가 질의만으로 경로를 재현·검증해야 한다. 신경망 판정은 재현 검증 대상이 될 수 없다.

실제 후보는 정규식 사전과 형태소 분석기(kiwi/mecab-ko) 둘뿐이고, 지연은 도입을 막는 이유가 아니다. 실측:

부정 가드만    평균 0.0022ms
분류기 전체    평균 0.2546ms  (최대 0.6ms)

같은 경로의 CEO_PLANNING_READ_TIMEOUT_SECONDS가 기본 2초, kanban CLI 서브프로세스와 부서 LLM 호출은 초 단위다. 형태소 분석기를 붙여도 짧은 문장은 0.1~1ms라 관측되지 않는다.

도입하지 않은 진짜 이유 세 가지:

  1. 모듈 계약 파괴ceo_query_routing은 "LangGraph·Hermes를 import 하지 않아 BFF 경계에서 그래프 없이 돈다"가 명시된 계약이다. 형태소 모델은 이걸 import하는 모든 프로세스(BFF·worker·supervisor·mirror)에 수백 MB 상주 비용과 콜드스타트를 얹는다. 요청당 지연이 아니라 프로세스당 비용이다.
  2. 재현성 — 사전/모델 버전이 바뀌어 토크나이즈가 달라지면 과거 기록된 경로가 갑자기 불일치로 뜨고 D5에 ROUTING_MISMATCH가 쌓인다. 정규식은 저장소에 텍스트로 박혀 diff로 추적되지만 모델 버전은 아니다.
  3. 컨테이너 이미지마다 의존성 추가.

재검토 트리거 (개발원칙 8): 실사용 질의에서, 안전·안내 같은 낱말을 깨지 않고는 정규식으로 표현할 수 없는 미탐이 나오면. 그때도 런타임 도입 전에 형태소 분석기를 개발 도구로만 쓰는 방법을 먼저 시도한다 — 빌드/CI에서 부정 형태 조합을 생성해 현재 사전의 커버리지를 검사하고, 배포 이미지에는 정규식만 넣는다. 지금 손으로 만든 260건 매트릭스를 분석기가 대신 만들어주는 형태다.

남은 개선 여지: 지금은 변형 처리가 정규식 곳곳에 흩어져 있다. 결정론적 정규화 패스 하나로 모으면(안하고|안 하고|안할래|못하게|않고⟨NEG⟩) 변형이 늘 때 고칠 곳이 한 군데가 되고, 나중에 형태소 분석기를 넣을 때도 그 패스만 교체하면 된다. 이번 PR 범위에는 넣지 않았다.


2. 사전이 진짜 약한 곳은 게이트가 아니라 부서 선택

"반도체 관련주 전망 어때?"가 되묻기로 떨어지는 건 전망·어때가 사전에 없어서다. 이건 관련성 판단이라 CLAUDE.md가 LLM 사용을 이미 허용한 영역이다. NLP를 넣는다면 주문 게이트가 아니라 여기다. 아래 미결 항목 2번과 같은 건이다.


3. 임의로 정하지 않고 남긴 결정 (expectedFailure 3건이 여기 걸려 있음)

CLAUDE.md "설계 공백을 임의로 채우지 않는다"에 해당. 담당자 결정이 필요하다.

  1. LLM 플래너 적용 범위 — 지금은 결정론이 못 정한 binding 잔여분만. "리밸런싱은 하지 말고 비중만 알려줘"가 여기로 간다. 범위를 넓히면 verify_primary_route의 전제가 깨지므로 ADR이 필요하다.
  2. 키워드 사전 확장 범위전망·투자·어때·될까 미포함. 사전을 넓힐지 LLM fallback으로 덮을지는 1번에 종속.
  3. "업무 브리핑" 카테고리 신설 여부 — 없어서 기본값 research+risk로 간다. "지금 보유 종목과 수익 보여줘"가 accounting으로 안 가는 것도 같은 뿌리.
  4. 순수 부정 문장 목적지"매수하지 마"가 지금 research+risk 카드 2장을 만든다. 되묻기인지 일반 CEO 대화 응답인지 정해야 되묻기 게이트의 부정 미인식을 닫을 수 있다.

4. 의도적으로 범위 밖에 둔 것

  • orchestration/workflows/portfolio_recommendation.py는 이관하지 않았다. 거기 build_ceo_task_plan은 레인 결정이 아니라 플랜 생산자(분류기가 내부에서 쓰는 그것)이고, 인자도 query 문자열이 아니라 profile 매핑이다. 이관하면 포트폴리오 그래프에 immediate_order 같은 주문 레인 어휘를 넘기게 된다.
  • ceo_supervisor.pyparse_analysis_then_conditional_paper_order 호출도 그대로 뒀다. 레인 결정이 아니라 이미 접수된 주문 record의 원문을 다시 파싱해 연계 2단계를 만드는 곳이다(권한은 record.ceo_root_task_id로 이미 확인됨).
  • conditional_rules/의 AST validator는 건드리지 않았다. 게이트를 닫는 것이 목적이지 규칙 검증을 바꾸는 것이 아니다.

5. Memo Harness(D5)에 미치는 영향

이 PR이 정본 경로를 바꾸므로 하네스에 두 가지가 따라온다.

  • 낡은 회귀 후보expected_primary_profiles는 기록 시점 값으로 굳고 다시 계산되지 않는다. 이 PR 이전에 부정 섞인 질의로 라우팅 불일치 후보가 쌓였다면, 그 행의 기대 경로는 옛 기준이다. 그 후보는 regression_test_target: tests/orchestration/test_ceo_bff_routing.py를 달고 "여기 회귀 테스트 추가하라"고 지시하므로, 그대로 따르면 지금 코드와 어긋나는 테스트를 쓰게 된다. 몇 건인지는 DB를 봐야 안다(0건일 수도 있다).
  • 새로 보이는 사례 — 부정 붙은 주문 문장이 주문 스코프를 벗어나면서, 전에는 하네스에 안 보이던 사례가 이제 D5에 기록된다.

배선 자체는 이번 변경으로 깨지지 않았다. D5 조회 게이트(order_route_requestedorder_grammar_detected)는 값 동치를 실측 확인했고, routing_basis 값과 플랜 키 집합은 전건 불변이다.

별건이지만 확인이 필요한 것: deploy/eb/docker-compose.ymlMEMOHARNESS_D5_MODE:-off다(docker-compose.yml·deploy/aws/active, .env.exampleactive). EB 경로로 떠 있으면 하네스는 조회도 기록도 하지 않는다. 이 PR과 무관하지만 누가 확인해야 한다.

@yellow-jam
yellow-jam merged commit cb5aba6 into main Aug 31, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant