Initial commit: ABAP indexing pipeline (ingest, parser, summarize, index, query, wiki)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,113 @@
|
||||
# 정의부(declaration) 수집 설계 — 2026-09-21
|
||||
|
||||
## 왜 필요한가
|
||||
|
||||
이 인덱스의 최종 용도는 **비슷한 로직을 새로 짤 때 기존 코드를 찾아 복사해 오는 것**이다.
|
||||
그런데 인덱스에 담긴 조각(logic_chunk)의 코드 원문은 **로직만**이다. ABAP 은 내부테이블·스트럭처·
|
||||
상수·필드심볼을 TOP 인클루드나 FORM 머리에 따로 선언하므로, 로직만 복사해 붙여넣으면
|
||||
`GT_LOAD 는 선언되지 않았습니다` 류의 문법 오류가 난다.
|
||||
|
||||
그래서 **정의부를 인덱싱 시점에 따로 수집**한다. 조각 코드에는 계속 로직만 둔다(검색 품질을
|
||||
지키기 위해서다 — 선언 수백 줄이 조각에 섞이면 조각의 뜻이 흐려진다).
|
||||
|
||||
수집한 정의부를 **쓸지 말지는 판단하지 않는다.** 붙여넣는 쪽의 LLM 이 정한다:
|
||||
이미 같은 선언이 있으면 버리고, 이름이 겹치면 바꾸고, 타입만 빌려 쓰기도 한다.
|
||||
인덱스는 "이 조각을 가져가려면 이것들이 함께 필요하다"는 **재료와 분류**만 정확히 넘긴다.
|
||||
|
||||
## 무엇을 수집하는가
|
||||
|
||||
`parser/declarations.py` 가 선언 문장을 **붙여넣을 수 있는 한 문장 단위**로 잘라 `declaration`
|
||||
테이블에 넣는다 (`index/db.py`).
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| kind | data / types / constants / tables / field-symbol / parameter / select-option / ranges / type-pool / controls / class / macro |
|
||||
| scope | `global`(TOP·공용 인클루드) / `unit`(FORM·METHOD 머리의 로컬 선언) |
|
||||
| code | **선언 원문** — 체인 항목이면 헤드를 다시 붙이고 `,` 를 `.` 로 닫아 유효한 문장으로 만든다 |
|
||||
| depends | 이 선언이 참조하는 다른 이름 (`LIKE GT_DATA` → GT_DATA, `TYPE TABLE OF TY_X` → TY_X) |
|
||||
|
||||
세 가지가 핵심이다.
|
||||
|
||||
1. **`BEGIN OF … END OF` 는 한 덩어리다.** 업무 프로그램의 내부테이블은 거의 전부 이 형태다
|
||||
(`DATA : BEGIN OF GT_UPLOAD OCCURS 0, … END OF GT_UPLOAD.`). `parser/dataflow.py` 의 심볼
|
||||
추출은 이걸 건너뛰지만(추적에는 이름만 필요하다), 정의부에는 반드시 통째로 있어야 한다.
|
||||
2. **체인 항목도 혼자 설 수 있어야 한다.** `DATA: a TYPE i,\n b TYPE i.` 의 `b` 만 잘라내면
|
||||
`b TYPE i.` 라 붙여넣을 수 없다 → `DATA:\n b TYPE i.` 로 헤드를 복원한다.
|
||||
이를 위해 `parser/statements.py` 가 체인 전개 시 **항목별 줄 범위**를 기록한다.
|
||||
3. **로컬 클래스·매크로도 정의부다.** `CLASS lcl_x DEFINITION … ENDCLASS`, `DEFINE … END-OF-DEFINITION`
|
||||
은 unit 통째를 한 건으로 담는다.
|
||||
|
||||
## 조각 → 정의부 조립 (`index/decls.py`)
|
||||
|
||||
1. 조각 코드의 식별자를 모은다 (파서 토크나이저 · 문장 단위)
|
||||
2. `declaration` 에서 찾는다 — **unit 로컬 먼저, 없으면 전역**
|
||||
3. 찾은 선언의 `depends` 를 **재귀로** 끌어온다 (`LT_DATA LIKE GT_DATA` → GT_DATA)
|
||||
4. 의존이 먼저 오도록(post-order) 정렬해 `declaration_code` 한 덩어리로 만든다
|
||||
|
||||
결과는 네 갈래로 분류해 넘긴다.
|
||||
|
||||
| 필드 | 뜻 | 붙여넣는 쪽이 할 일 |
|
||||
|---|---|---|
|
||||
| `declarations` / `declaration_code` | 함께 가야 하는 선언 원문 | 있으면 버리거나, 이름을 바꿔 쓰거나, 그대로 넣는다 |
|
||||
| `declaration_external_refs` | DDIC 사전 객체 (BKPF, LVC_T_FCAT …) | 가져갈 게 없다. 대상 시스템에 있는지만 본다 |
|
||||
| `declaration_params` | 이 조각이 쓰는 FORM 파라미터 | 호출 측에서 넘어오는 값이다 |
|
||||
| `declaration_unresolved` | 변수처럼 생겼는데 선언을 못 찾은 이름 | 수집 안 된 인클루드·함수 인터페이스일 수 있다 |
|
||||
|
||||
### 판정에서 가른 두 가지
|
||||
|
||||
- **`LIKE BSEG-WRBTR` 은 작업영역이 아니다.** 사전에서 타입만 빌려 쓰는 것이라
|
||||
TOP 의 `TABLES: BSEG.` 를 딸려 보내면 안 된다. 반면 `bseg-wrbtr = 100.` 처럼 **작업영역을
|
||||
실제로 쓰면** 그 선언이 필요하다. 그래서 참조를 컴포넌트까지 보존해 둘을 구분한다.
|
||||
- **호출문의 형식 파라미터는 변수가 아니다.** `CALL FUNCTION 'X' EXPORTING i_bukrs = gv_bukrs`
|
||||
의 `i_bukrs` 와 `EXCEPTIONS no_rate_found` 는 호출 대상의 인터페이스다. 이름만 보면 변수와
|
||||
구분되지 않아(둘 다 `I_`/`NO_`) 문장 구조로 걸러낸다.
|
||||
|
||||
### 저장하지 않고 읽을 때 계산한다
|
||||
|
||||
조각↔선언 대응은 `logic_chunk` 에 캐시하지 않는다. `declaration` 과 조각 코드만 있으면
|
||||
결정론적으로 다시 나오므로, 캐시하면 조각 재추출·선언 변경과 엇갈려 따로 늙기만 한다.
|
||||
조각 하나당 수십 줄을 토큰화하는 비용이라 조회 경로에서 충분히 싸다.
|
||||
|
||||
## 실측 (샘플 55본 / 조각 3,212건, 2026-09-21)
|
||||
|
||||
- 선언 18,049건 수집 (data 11.7k · constants 1.7k · field-symbol 1.0k · tables 1.0k · type-pool 488 · macro 295 · class 91 …)
|
||||
- 조각의 **79%** 가 정의부를 갖는다 (평균 3.5건, 최대 21건).
|
||||
나머지 21%는 호출·제어·메시지처럼 프로그램 선언을 쓰지 않는 조각이다.
|
||||
- 선언을 못 찾은 이름이 남은 조각 **3%** — DDIC 테이블(J_1BBRANCH), 함수 인터페이스 파라미터,
|
||||
화면 필드가 대부분이다.
|
||||
|
||||
### 곁가지로 고친 파서 버그 3건
|
||||
|
||||
정의부를 조립하면서 "선언이 없다"고 새어 나온 이름들을 좇아가 드러난 것들이다.
|
||||
|
||||
1. `CLASS lcl_x DEFINITION DEFERRED.` 를 블록으로 열어 **그 뒤 인클루드 전체를 CLASS_DEF 하나로
|
||||
삼켰다** (ENDCLASS 가 없다). ZCO_ALV 의 선언 수십 건이 통째로 사라져 있었다.
|
||||
2. FORM 시그니처의 파라미터를 **첫 개만** 잡았다 (`USING p_date LIKE x p_days LIKE y` 에서
|
||||
TYPE/LIKE 를 만나면 멈췄다).
|
||||
3. `PARAMETER`(단수형)를 선언 키워드로 몰랐다 — 미인식 문장 123건 → 3건.
|
||||
|
||||
## API
|
||||
|
||||
| Method | Path | 정의부 관련 |
|
||||
|---|---|---|
|
||||
| GET | `/chunks/{chunk_id}` | `declarations`, `declaration_code`, `declaration_external_refs`, `declaration_params`, `declaration_unresolved` (끄려면 `?decls=false`) |
|
||||
| GET | `/programs/{name}/units/{unit}/code` | 같은 필드 (unit 통째로 가져갈 때) |
|
||||
| GET | `/programs/{name}/declarations?scope=global\|unit` | 프로그램의 선언 카탈로그 |
|
||||
|
||||
## 위키에서의 표시
|
||||
|
||||
원문은 프로그램 문서의 `## 정의부` 섹션에 **한 번만** 싣는다 (인클루드별, 전역 선언 전체 +
|
||||
조각이 실제로 쓰는 unit 로컬 선언). 조각 쪽에는 이름만 두고 그 항목으로 가는 **문서 내 링크**를 건다.
|
||||
|
||||
```markdown
|
||||
- 정의부(복사 시 함께 필요): [`GT_LOAD`](#decl-zfic0020top-6-gt_load), [`C_X`](#decl-zficom-213-c_x)
|
||||
…
|
||||
#### GT_LOAD — data · L6-L56 {#decl-zfic0020top-6-gt_load}
|
||||
```
|
||||
|
||||
`{#id}` 는 제목에 앵커를 다는 표기이고, 뷰어(`wiki_out/viewer.html`)가 이걸 `id` 로 바꿔
|
||||
눌러서 이동하게 만든다(이동 후 1.4초 강조). **뷰어 전용 기능이 아니다** — 마크다운으로 읽으면
|
||||
링크 텍스트가 곧 선언 이름이라 LLM 에게는 지금까지와 똑같이 읽히고, 앵커는 무시된다.
|
||||
|
||||
링크와 앵커가 어긋나면 조용히 죽으므로 짝을 테스트로 묶어 뒀다
|
||||
(`tests/test_wiki_out.py::test_chunk_links_to_declaration`). 실측 11,051개 링크 / 깨진 링크 0개.
|
||||
@@ -0,0 +1,91 @@
|
||||
# 로직 조각(logic chunk) 추출 설계 — 2026-09-16 구조 변경
|
||||
|
||||
## 결정
|
||||
|
||||
인덱스의 단위를 **파서가 자른 unit(FORM/METHOD/FUNCTION/MODULE/이벤트)** 에서
|
||||
**LLM 이 골라낸 로직 조각(logic chunk)** 으로 바꾼다.
|
||||
|
||||
- 파서 unit 은 더 이상 인덱스 단위가 아니다. LLM 에게 코드를 **나눠 보여주는 창(window)** 이자,
|
||||
조각의 위치를 설명하는 **컨테이너** 역할만 남는다.
|
||||
- LLM 은 unit 코드를 읽고 업무적으로 의미 있는 로직 조각(특정 SQL, DB 갱신, BAPI 호출,
|
||||
집계 LOOP, 검증 …)을 0개 이상 골라내 각각에 자연어 설명을 붙인다.
|
||||
- 의미 없는 코드(선언, 초기화, ALV 필드카탈로그, 화면 속성 …)는 조각으로 만들지 않는다 → 인덱스에서 버려진다.
|
||||
- 조각의 설명·키워드가 검색(chunk_fts)의 1차 대상이다.
|
||||
|
||||
## 왜 프로그램 전체가 아니라 unit 하나씩 보여주는가
|
||||
|
||||
| 관점 | 프로그램 전체 1회 | unit 창 + 프로그램 요약 문맥 (채택) |
|
||||
|---|---|---|
|
||||
| 긴 코드에서의 추출 품질 | 수천 줄에서 뒤쪽을 대충 봄 | 200~300줄 단위라 안정적 |
|
||||
| 줄 번호 확정 | 어려움 | 창 안이라 매칭 쉬움 |
|
||||
| 증분 비용 | 한 줄 수정에도 전체 재실행 | 바뀐 unit 만 |
|
||||
| unit 을 가로지르는 로직 | 잡을 수 있음 | **프로그램 요약(main_flow) 이 담당** |
|
||||
|
||||
그래서 2단계로 간다.
|
||||
|
||||
1. **프로그램 요약** — 구조 사실(이벤트 흐름, unit 목록, 테이블, 외부 호출, 텍스트 심볼, 이벤트 블록 코드)로
|
||||
`ProgramSummary` 를 만든다. 코드가 바뀌면(`summary_status='stale'`) 다시 만든다.
|
||||
2. **unit 별 조각 추출** — 프로그램 요약 + unit 코드(줄번호 포함) + 파서 힌트(DB 접근·호출 줄)를 주고
|
||||
`UnitExtraction { unit_purpose_ko, chunks[] }` 를 받는다.
|
||||
300줄을 넘는 unit 은 파서의 `sub_chunks`(최상위 IF/LOOP 경계) 창으로 나눠 여러 번 호출한다.
|
||||
|
||||
## 사실은 파서가, 해석은 LLM 이 — 조각 단위에서의 적용
|
||||
|
||||
- **줄 번호**: LLM 이 준 `line_start/line_end` 는 `first_line`(시작 줄 코드 원문)과 대조해 확정한다.
|
||||
어긋나면 unit 안에서 그 줄을 찾아 보정하고, 못 찾으면 조각을 버린다(`dropped`).
|
||||
- **테이블/호출**: 조각 코드 범위를 파서(`split_statements` + `extract_refs`)로 다시 돌려
|
||||
`tables_read/tables_write/calls` 를 채운다. LLM 이 쓴 값은 쓰지 않는다.
|
||||
- **해시**: 조각 코드의 sha256 을 `code_hash` 로 저장한다. unit 의 code_hash 가 같으면 재적재 시 조각을 보존한다.
|
||||
|
||||
## 스키마 (index/db.py)
|
||||
|
||||
```
|
||||
logic_chunk(chunk_id PK = <unit_id>#C<seq>, program, include, unit_id, seq,
|
||||
line_start, line_end, code_hash, kind, purpose_ko, purpose_en,
|
||||
keywords_ko, keywords_en, sap_objects, tables_read, tables_write, calls (JSON),
|
||||
confidence, prompt_version, extracted_at)
|
||||
chunk_fts(chunk_id, program, purpose, keywords, objects, bigrams) -- FTS5
|
||||
unit.summary_json = {purpose_ko(한 줄), chunk_count, covered_lines, coverage} -- 얇은 unit 색인
|
||||
unit.summary_status = none | done | failed (done 이면서 chunk_count=0 = 의미 조각 없음)
|
||||
program.summary_json = ProgramSummary, program.text_symbols_json
|
||||
```
|
||||
|
||||
`kind` 어휘: sql_select, db_write, fm_call(BAPI/FM/RFC), aggregation(내부테이블 집계·가공),
|
||||
validation(검증·권한), calculation, output(ALV·화면·파일·메일), interface, control_flow, other
|
||||
|
||||
## 검색 (query/tools.py)
|
||||
|
||||
- `search_logic(q)` → chunk_fts 검색 → **프로그램 단위로 묶어** 반환(프로그램 요약 + 조각 목록).
|
||||
- `search_units`/`search_programs` 는 얇은 색인으로 유지 — 버려진 코드도 unit 이름·주석·한 줄 요약으로는 도달 가능.
|
||||
- `get_chunk(chunk_id)` → 메타 + 코드 원문.
|
||||
|
||||
## 정의부 — 조각을 붙여넣을 때 필요한 선언
|
||||
|
||||
조각 코드에는 **로직만** 둔다. 선언을 섞으면 조각의 뜻이 흐려지고 검색이 나빠진다.
|
||||
대신 `declaration` 테이블에 선언 원문을 따로 모아 두고, 조각을 꺼낼 때 그 조각이 쓰는 선언을
|
||||
의존까지 묶어 조립해 함께 준다 (`get_chunk` → `declaration_code`).
|
||||
→ `docs/definition-block-design.md`
|
||||
|
||||
## 위키 (wiki_out)
|
||||
|
||||
- units/ 문서 생성 중단. 프로그램 문서 안에 `## 로직 조각` 섹션(조각마다 H3, resource 줄 범위)으로 넣는다.
|
||||
→ 1만 프로그램 × 수십 unit 파일 문제 해소.
|
||||
- OKF frontmatter 에 `x-chunk-count` 추가.
|
||||
|
||||
## 평가 (eval)
|
||||
|
||||
- 질문 유형 `logic` 추가: `expected` = `[{program, line_from, line_to}]`.
|
||||
조각 검색 상위 10개 중 프로그램이 같고 줄 범위가 겹치면 적중.
|
||||
|
||||
## 남은 일 (구조 변경 뒤 10개 보강 과제와 연결)
|
||||
|
||||
2026-09-16 에 9건 반영 완료 — 판정·적용 내역은 `docs/수정사항-적용.md`.
|
||||
|
||||
- ✅ 조각 키워드·요약을 program_fts 에 반영(과제 1) — `index/loader.py:refresh_program_fts`
|
||||
- ✅ 텍스트 심볼 FTS 반영(과제 3) — `index/db.py:text_symbol_phrases`
|
||||
- ✅ 용어 사전 질의 확장(과제 4) — `query/expand.py` (2단 검색)
|
||||
- ✅ 중복 조각 해시 묶기(과제 6) — `summarize/runner.py:clone_unit_chunks` (줄 평행이동 포함)
|
||||
- ✅ 병렬 실행(과제 10) — 프롬프트 조립/호출/저장 3단 분리 + 429 백오프
|
||||
- ✅ 엔티티 위키 + 계층 index.md(과제 8) — `wiki_out/entities.py`
|
||||
- ⏸ **정답셋 확대(과제 7)** — 남은 유일한 과제. 이게 없으면 위 검색 변경의 정밀도를 검증할 수 없다.
|
||||
- ⏸ 프로세스 페이지(과제 8 후반) — 프로그램을 가로지르는 reduce 단계. 조각이 쌓인 뒤 판단.
|
||||
@@ -0,0 +1,39 @@
|
||||
# OKF 스펙 버전 고정 기록 (계획서 v4 §10.11)
|
||||
|
||||
- **채택 버전**: OKF v0.2
|
||||
- **확인일**: 2026-08-26
|
||||
- **원문**: https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md
|
||||
- (v0.1: 2026-06-12 공개 / v0.2: 2026-07-25 — 신뢰 필드 추가)
|
||||
|
||||
## 확인된 스펙 요지
|
||||
|
||||
- 필수 필드는 `type` 하나. 권장: `title`, `description`, `resource`, `tags`.
|
||||
- 신뢰/수명 필드(선택): `generated: {by, at}`, `verified: [{by, at}]`(단일 매핑도 1원소 리스트로 해석),
|
||||
`sources`(provenance), `status: draft|stable|deprecated`(기본 stable), `stale_after`.
|
||||
- actor 표기: 에이전트/도구 `<producer>/<version>`, 사람 `human:<id>`, 프로세스 `process:<id>`.
|
||||
- **확장 필드 자유** — 소비자는 미지 키를 보존해야 하고, 없는 선택 필드 때문에 거부하면 안 됨.
|
||||
- 예약 파일: 번들 루트 `index.md`(매니페스트, `okf_version: "0.2"` frontmatter), `log.md`.
|
||||
→ 구 계획(v3)의 `okf.yaml` 매니페스트는 **index.md 로 대체**.
|
||||
- 링크: 표준 마크다운 링크. `/` 로 시작하면 번들 루트 상대(권장 — 문서 이동에 안정적).
|
||||
|
||||
## 계획서 x- 필드 → v0.2 표준 필드 매핑 (확정)
|
||||
|
||||
| 구 계획(v3) | 채택(v4) | 비고 |
|
||||
|---|---|---|
|
||||
| `x-human-edited: true` | `verified: { by: "human:<id>", at: <ISO-8601> }` | 사람이 검토·수정 후 저장할 때 추가. merge.py 는 구 규약도 하위호환 인식 |
|
||||
| (없음) | `generated: { by: "abap-indexing/<모델>", at: ... }` | 생성 주체. 요약 전 구조 문서는 `abap-indexing/parser` |
|
||||
| 재검토 필요 표시 | `status: draft` + `wiki/_review.md` | 코드 해시 변경 시 merge.py 가 설정 |
|
||||
| `x-confidence`, `x-prompt-version`, `x-program` 등 사실 필드 | **x- 확장 유지** | 표준에 대응 없음 — 스펙이 확장 키를 허용 |
|
||||
| `okf.yaml` | `index.md` (예약 파일) | wiki_out/okf_writer.py `write_manifest` |
|
||||
|
||||
## 구현 위치
|
||||
|
||||
- 출력: `wiki_out/okf_writer.py` / 병합: `wiki_out/merge.py` / 검증: `python -m wiki_out.validate`
|
||||
- frontmatter 의 관리 대상(배치가 갱신하는) 필드는 `merge.MANAGED_KEYS` — 모두 한 줄 YAML 로 emit.
|
||||
|
||||
## 2026-09-16 변경 — 문서 단위
|
||||
|
||||
- unit 별 개별 문서(`units/`, `type: abap-unit`)는 생성하지 않는다.
|
||||
- LLM 이 골라낸 로직 조각은 프로그램 문서(`type: abap-program`) 안의 `## 로직 조각` 섹션이다.
|
||||
조각마다 resource 는 `abap://<PROG>/<INCLUDE>#L<from>-L<to>` 형식으로 본문에 적는다.
|
||||
- 확장 필드 `x-chunk-count` 추가. 설계 근거는 `docs/logic-chunk-design.md`.
|
||||
@@ -0,0 +1,67 @@
|
||||
# 실행 기록 — 어느 모델이 무엇을 만들었나
|
||||
|
||||
다른 모델로 같은 작업을 다시 돌려 비교하기 위한 기록이다. 비교 기준선(baseline)은 아래 상태다.
|
||||
|
||||
## 모델
|
||||
|
||||
| 범위 | 모델 |
|
||||
|---|---|
|
||||
| **인덱싱 파이프라인 본체** (Stage 1~6: 정규화 · 파서 · 적재 · 요약/조각 추출 · 질의 API · 위키/뷰어) | **Fable 5.1** |
|
||||
| 정의부(declaration) 수집·조립 기능 (2026-09-21, `docs/definition-block-design.md`) | Opus 5 (1M context, `claude-opus-5[1m]`) |
|
||||
|
||||
이 저장소에 있는 것의 **거의 전부는 Fable 5.1 이 만들었다.** 정의부 작업은 그 위에 얹은 한 기능이다.
|
||||
|
||||
## 기준선 지표 (2026-09-21 기준)
|
||||
|
||||
샘플 55본으로 전 단계를 돌린 결과다. 재실행 결과를 여기에 대보면 된다.
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 프로그램 / unit / 문장 | 55 · 6,296 · 95,401 |
|
||||
| 파서 미인식 문장 | 3건 (0.003%) |
|
||||
| 로직 조각 (LLM 추출) | 3,212건 |
|
||||
| 선언(정의부) | 18,049건 — 조각의 79%가 정의부를 가짐 (평균 3.5건) |
|
||||
| 위키 | 프로그램 55 · 테이블 160 · 펑션 119 · 개념 23 · T-code 17 |
|
||||
| 테스트 | 99 passed / 6 skipped (6건은 ZFIR10070 정규화 산출물이 없어 skip) |
|
||||
| index.db | 약 43 MB |
|
||||
|
||||
## 재실행할 때 주의
|
||||
|
||||
- **이 저장소 상태 위에서 같은 프롬프트를 돌리면 비교가 안 된다.** 결과물(코드·위키·DB)이 이미
|
||||
있어서 다음 모델은 "무엇을 만들어야 하는지"를 읽고 시작하게 된다. 같은 출발선에서 재려면
|
||||
변경 전 상태를 따로 떠 두고 거기서 돌려야 한다 (이 저장소는 git 관리가 아니다).
|
||||
- Stage 3(조각 추출)은 LLM 호출이라 **모델이 바뀌면 조각의 개수·경계·설명이 달라진다.**
|
||||
Stage 1·2·4(정규화·파서·적재)는 LLM 없이 결정론적이므로 코드가 같으면 결과도 같아야 한다.
|
||||
즉 모델 비교가 실제로 갈리는 지점은 **코드를 어떻게 짜는가**와 **조각 추출 품질**이다.
|
||||
- 조각 추출을 다시 돌릴 거면 `PROMPT_VERSION`(summarize/runner.py)과 `.env` 의 모델 설정을
|
||||
함께 기록해 둘 것 — 그게 없으면 나중에 어느 조각이 어느 모델 산출인지 구분되지 않는다.
|
||||
|
||||
## GLM 5.2 비용 실측 (2026-09-21)
|
||||
|
||||
비교 실행 대상은 OpenRouter `z-ai/glm-5.2`. 전체 실행은 **잔액 부족(402)** 으로 못 돌렸고,
|
||||
실제 프롬프트 5건을 GLM 5.2 로 직접 호출해 단가를 쟀다 (총 $0.024 소요).
|
||||
|
||||
| 측정값 | 결과 |
|
||||
|---|---|
|
||||
| 문자/토큰 (한국어+ABAP 혼합) | 1.97 ~ 2.1 |
|
||||
| 호출 1회 (프롬프트 3.1k~5.1k자) | 입력 1,643~2,577 토큰 / 출력 130~1,639 토큰 |
|
||||
| 그중 **추론(reasoning) 토큰** | 출력의 58~68% — GLM 5.2 는 추론 모델이라 비용의 큰 몫 |
|
||||
| 프롬프트 1,000자당 비용 | $0.00071 (관측 범위 $0.0005~$0.0012) |
|
||||
| 단가 | 입력 $0.6496/M · 출력 $2.0416/M |
|
||||
|
||||
이 단가를 현재 코퍼스의 **실제 프롬프트 5,642건(23.3M자)** 에 적용한 예측:
|
||||
|
||||
| 범위 | 비용 |
|
||||
|---|---|
|
||||
| 프로그램 1본 (중앙값 120호출·446k자) | **$0.32** (범위 $0.22~$0.52) |
|
||||
| 현재 코퍼스 55본 전체 | **$16.5** (범위 $11.6~$27.0) |
|
||||
| $10 으로 가능한 양 | **프로그램 약 32본** / LLM 호출 약 3,400회 |
|
||||
|
||||
- 호출 수는 파서가 정하므로(unit 수) 모델이 바뀌어도 같다. 달라지는 건 토큰 단가와 추론 길이다.
|
||||
- 추론 길이 편차가 커서 범위가 넓다(같은 크기 프롬프트에서 출력 130~1,639 토큰).
|
||||
- 더 싼 슬러그 환산: `z-ai/glm-4.7` 약 $12.5, `z-ai/glm-5.3-flash` 약 $2.4 (같은 코퍼스 전체).
|
||||
- `summarize/llm_client.py` 가 `max_tokens` 를 안 보낸다. OpenRouter 는 최대 출력 기준으로
|
||||
선결제 견적을 잡으므로 잔액이 적으면 402 가 난다. 추론 폭주 방어도 겸해 상한을 두는 게 좋다.
|
||||
|
||||
실행용 폴더: `C:\EdgeCenter\abap-indexing-glm52` (코드 + data/raw 만 복사, Stage 1·2·4 완료,
|
||||
ZFIR10070 1본 적재 = unit 122·문장 1,756). 키만 채우면 Stage 3 부터 바로 돌릴 수 있다.
|
||||
Binary file not shown.
+182
@@ -0,0 +1,182 @@
|
||||
|
||||
# 수정사항 10건 검토·적용 기록 (2026-09-16)
|
||||
|
||||
원본: `수정사항.txt`. 이 파일은 **2026-09-16 구조 변경(로직 조각 도입) 이전** 기준으로 작성돼
|
||||
일부 항목은 이미 해소된 상태였다. 항목별 판정과 적용 내용을 남긴다.
|
||||
|
||||
| # | 항목 | 판정 | 적용 |
|
||||
|---|---|---|---|
|
||||
| 1 | LLM 요약이 검색 색인에 미반영 | 부분 유효 | ✅ 적용 |
|
||||
| 2 | 프로그램 단위 요약 미생성 | **이미 완료** | 코드 변경 없음 |
|
||||
| 3 | 텍스트 심볼 치환 | 프롬프트 완료 / FTS 미반영 | ✅ FTS 반영 |
|
||||
| 4 | 용어 사전이 질의 확장에 미사용 | 유효 | ✅ 적용 |
|
||||
| 5 | keywords_en · sap_objects 필드 없음 | **이미 완료** | 코드 변경 없음 |
|
||||
| 6 | 중복 프로그램 처리 없음 | 유효 | ✅ 적용 |
|
||||
| 7 | 정답셋 4문항 | 유효 | ⏸ **범위 제외** (사용자 지시) |
|
||||
| 8 | 엔티티·프로세스 위키 페이지 없음 | 유효 | ✅ 엔티티 적용 / 프로세스 보류 |
|
||||
| 9 | unit 문서 70만 파일 문제 | **구조 변경으로 해소** | 무효 |
|
||||
| 10 | 요약 병렬 실행 없음 | 유효 | ✅ 적용 |
|
||||
|
||||
## 이미 완료였던 항목 (2·5·9)
|
||||
|
||||
- **2번** — `summarize/runner.py:ensure_program_summary()` 가 `ProgramSummary` 를 만들어
|
||||
`program.summary_json` 에 저장하고, `UNIT_PROMPT` 에 `program_purpose` · `main_flow` ·
|
||||
`key_internal_tables` 를 문맥으로 주입한다. DB 에 요약이 0건이었던 것은 미구현이 아니라
|
||||
OpenRouter 무료 티어의 **429 Too Many Requests** 로 실행이 실패해서였다 (`unit.summary_error` 6건 전부 429).
|
||||
- **5번** — `schemas.LogicChunk` 에 `purpose_en` · `keywords_ko` · `keywords_en` · `sap_objects` 가
|
||||
모두 있고 프롬프트가 명시적으로 요구한다.
|
||||
- **9번** — `units/` 문서 생성은 중단됐다. 로직 조각은 프로그램 문서 안의 H3/H4 섹션이다.
|
||||
원본 파일의 "units/ 를 git 제외로 둔 것은 좋은 완화책" 서술은 현재 상태와 맞지 않는다
|
||||
(`.gitignore` 에 `units` 항목이 없고 디렉토리도 없다).
|
||||
|
||||
## 적용 내용
|
||||
|
||||
### 1번 — 요약을 검색 색인에 반영
|
||||
|
||||
구조 변경으로 `chunk_fts` 가 신설돼 조각의 `purpose`/`keywords`/`objects` 는 이미 색인되고 있었고,
|
||||
`extract_unit` 도 요약 후 `unit_fts` 를 갱신하고 있었다. 남아 있던 두 구멍만 막았다.
|
||||
|
||||
- **적재 시점 `unit_fts.purpose` 의 장식 문자** — `index/loader.py` 가 `header_comment` 를 날것으로
|
||||
넣어 1,177행 중 **867행이 `'----------------'`** 이었다. `clean_comment()` 를 적용해 0행으로.
|
||||
- `clean_comment` 를 `query/tools.py` → `index/db.py` 로 이동 (loader 가 써야 하는데
|
||||
`index → query` 는 의존 역방향). `query.tools.clean_comment` 이름은 하위호환으로 유지.
|
||||
- **`program_fts` 가 타이틀·패키지명뿐** — `business_purpose_ko` / `main_flow` / `keywords_ko` /
|
||||
`keywords_en` / `business_tags` / `sap_module` / `related_tcodes` / 조각 키워드 / 조각 SAP 객체 /
|
||||
테이블명 / 텍스트 심볼을 색인에 넣었다.
|
||||
- **색인을 두 테이블로 나눴다**: `program_fts`(이름·타이틀·패키지) + `program_desc_fts`(서술).
|
||||
한 행에 몰아넣었더니 회귀가 났다 — bm25 는 **행 전체 길이로 정규화**하므로 요약이 붙어
|
||||
길어진 행이 타이틀만 있는 짧은 행보다 낮게 나온다. 실측으로 타이틀이 정확히 `"총계정원장 조회"`인
|
||||
ZFIR10070 이 `"총계정원장 조회하는 프로그램"` 질의에서 **1위 → 33위**로 밀렸다
|
||||
(행 길이 1,872자 vs 40자). 컬럼 가중치로는 해결되지 않는다(정규화는 행 단위).
|
||||
나누면 각 테이블 안 행 길이가 비슷해지고, 서술 히트는 `_DESC_WEIGHT=0.6` 으로 **더해질 뿐**
|
||||
이름·타이틀 일치를 밀어내지 않는다. 회귀 테스트:
|
||||
`tests/test_dedupe_parallel.py::test_long_summary_does_not_outrank_title_match`
|
||||
- 세 FTS 모두 `rank`(모든 컬럼 가중치 1) 대신 **가중 bm25**(`index/db.py:bm25_rank`)를 쓴다.
|
||||
- FTS5 는 컬럼 추가가 불가하므로 `index/db.py:_migrate_fts()` 가 컬럼 구성 변화를 감지해
|
||||
DROP→재생성한다 (전부 파생 데이터라 안전).
|
||||
- `refresh_program_fts(con, program)` 단건 경로를 추가했다. 전량 재구축을 `/ingest` 마다 돌리면
|
||||
1만 본에서 O(N²) 가 된다.
|
||||
- 검색 결과의 `reason` 이 근거를 구분한다: `이름/타이틀 일치` > `키워드 일치` > `요약 일치` > `동의어 확장`.
|
||||
|
||||
### 3번 — 텍스트 심볼을 FTS 에 반영
|
||||
|
||||
- `index/db.py:text_symbol_phrases()` — 코드 범위에 등장하는 `TEXT-nnn` 을 한국어 원문으로 치환.
|
||||
- 조각 저장 시 `chunk_fts.keywords` / `bigrams` 에, 프로그램 색인에는 `program_fts.purpose` 에 넣는다.
|
||||
- 결과: `"계정코드를 입력하세요"`, `"회계단위 간편선택"` 같은 **화면 문구로도** 검색된다 (LLM 무관).
|
||||
|
||||
### 4번 — 용어 사전 질의 확장
|
||||
|
||||
- `config/glossary.py` — 사전 로딩을 한 곳으로 모았다. 기존에는 `wiki_out/run.py` 안에
|
||||
인라인 정규식으로만 있어 검색 쪽에서 쓸 수 없었다. **양방향 동의어 맵**을 만든다
|
||||
(`입고`→`GR/MSEG/101` 뿐 아니라 `GR`→`입고/MSEG` 도 성립).
|
||||
- `query/expand.py` — **2단 검색**. 원질의로 먼저 찾고, 결과가 `top_k` 미만일 때만 동의어 식으로
|
||||
보충하며 점수에 `EXPANDED_WEIGHT=0.35` 감쇠를 걸고 `matched_by='동의어 확장'` 으로 표시한다.
|
||||
- 동의어를 원질의와 같은 OR 버킷에 섞지 않은 이유: `fts_or` 는 토큰과 한글 2-gram 을 전부 OR 로
|
||||
이어 이미 재현율 편향이다. 여기에 동의어까지 같은 가중치로 넣으면 `"총계정원장"` 질의가
|
||||
`"원장"` 2-gram 하나로 걸린 문서와 동일 점수가 된다.
|
||||
- `config/domain_glossary.yaml` 에 MM 섹션(입고·출고·자재·구매오더·이동유형 …)을 시드했다.
|
||||
기존 사전이 FI 전용이라 원본 파일이 예로 든 "입고 → GR/101" 이 실제로는 동작하지 않았다.
|
||||
|
||||
### 6번 — 중복 unit 조각 복제
|
||||
|
||||
- `summarize/runner.py:_dedupe_plan()` + `clone_unit_chunks()`.
|
||||
- `code_hash` 가 같은 unit 은 **대표 1건만 LLM 에 보내고** 나머지는 조각을 복제한다.
|
||||
이번 배치 밖에서 이미 추출된 동일 코드 unit 이 있으면 **LLM 호출 0회**로 복제한다.
|
||||
- **줄 번호 평행이동이 핵심이다** — 코드는 같아도 include 안 위치가 달라
|
||||
`target.line_start - rep.line_start` 만큼 밀어야 한다. 테스트가 복제된 줄 범위의 코드 원문이
|
||||
대표와 동일한지 확인한다.
|
||||
- 실측(샘플 14본): 중복 그룹 108개 / unit 287개 → **LLM 호출 179회(62%) 절감 가능**.
|
||||
- `--no-dedupe` 로 끌 수 있다.
|
||||
|
||||
### 8번 — 엔티티 위키 페이지
|
||||
|
||||
- `wiki_out/entities.py` — `tables/` `functions/` `tcodes/` 를 `table_ref` · `call_edge` 에서
|
||||
**LLM 없이 결정론적으로** 생성. 실측 산출: 테이블 146건, 펑션 94건.
|
||||
- `tables/<T>.md` — 읽는 프로그램 / 쓰는 프로그램 / unit 단위 사용 지점
|
||||
- `functions/<FM>.md` — 호출 프로그램 / 호출 지점 / RFC 여부
|
||||
- `tcodes/` — `tcode` 테이블 미확보로 현재 0건 (데이터가 들어오면 그대로 생성됨)
|
||||
- 프로그램당 파일이 아니라 **엔티티당 파일**이라 9번의 파일 수 폭발이 재발하지 않는다.
|
||||
- `index.md` 를 계층 진입점으로 재작성 — **SAP 모듈 → 패키지 → 프로그램**.
|
||||
모듈은 요약의 `sap_module`, 요약 전에는 `Z<모듈><번호>` 관행에서 추정한다.
|
||||
- **프로세스 페이지는 보류.** 프로그램을 가로지르는 reduce 단계가 필요하고, 조각 데이터가
|
||||
실제로 쌓인 뒤에 판단하는 게 맞다 (원본 파일도 "별도 작업"으로 인정).
|
||||
|
||||
### 10번 — 병렬 실행
|
||||
|
||||
- `llm_concurrency` 가 **선언만 있고 읽는 코드가 없는 죽은 설정**이었다. 실제로 연결했다.
|
||||
- sqlite 커넥션은 스레드 간 공유가 안 되므로 3단으로 갈랐다:
|
||||
`build_unit_prompts()` (DB 읽기, 메인) → `_run_calls_parallel()` (LLM 호출, 워커) →
|
||||
`finish_unit()` (검증·저장, 메인).
|
||||
- **동시성만 올리면 429 가 늘어난다.** `OpenAICompatClient.complete_json` 에 429/5xx 지수 백오프
|
||||
재시도(`Retry-After` 존중, jitter 포함)를 같이 넣었다. 기존에는 재시도가 없어 429 한 번에
|
||||
unit 이 `failed` 로 굳었다.
|
||||
|
||||
## 함께 고친 것 (직전 분석에서 보고한 버그)
|
||||
|
||||
### 파서 토큰화 — 필드심볼·테이블본문 대입
|
||||
|
||||
`<ls_fcat>-fieldname = 'X'` 가 `['<ls_fcat>', '-', 'fieldname', '=', ...]` 로 쪼개져
|
||||
일반 대입 판정(`up[1] == "="`)이 깨졌다. 결과로 **쓰기 지점이 누락**되고 미인식 문장으로 잡혔다.
|
||||
|
||||
- `parser/tokenizer.py` — 필드심볼이 `-컴포넌트` / `->메서드` 까지 한 토큰. `&1` 매크로 파라미터도 토큰화.
|
||||
- `parser/statements.py:assignment_eq_index()` — 대입 판정을 한 곳으로. `X = `, `X[] = `,
|
||||
`X[ key = v ] = ` 형태를 모두 인식한다. `dataflow` 와 `run`(미인식 리포트)이 같은 판정을 쓴다.
|
||||
- 효과 (샘플 14본 / 문장 24,578건. 베이스라인 커밋 `35a15aa` 를 worktree 로 꺼내 직접 측정):
|
||||
|
||||
| 지표 | 이전 | 이후 | 차이 |
|
||||
|---|---|---|---|
|
||||
| 미인식 문장 | 1,055건 (4.29%) | **30건 (0.12%)** | −1,025 |
|
||||
| 최악 프로그램 | ZMMR71110_API **12.03%** | ZBACKUP_FI **0.46%** | |
|
||||
| 쓰기 지점 총계 | 7,327 | **8,251** | **+924** |
|
||||
| └ 필드심볼 대상 | 207 | **1,067** | **+860** |
|
||||
| └ `itab[]` 형태 | 230 | **307** | **+77** |
|
||||
|
||||
`itab[] = ...` 와 `<fs>-comp = ...` 는 내부테이블을 채우는 흔한 관용구라
|
||||
`trace_variable`("gt_head 가 어디서 채워지나")의 정확도에 직접 영향이 있었다.
|
||||
이전에도 필드심볼 쓰기가 207건 잡혔던 것은 `READ TABLE ... ASSIGNING <fs>` /
|
||||
`LOOP AT ... ASSIGNING <fs>` 경로는 별도 분기라 영향을 받지 않았기 때문이다.
|
||||
|
||||
### 외부 호출 목록 오염
|
||||
|
||||
`CL_GUI_ALV_GRID=>MC_FC_AUF` 같은 **정적 상수 읽기**가 `method_ref` 로 기록돼 호출로 취급됐다.
|
||||
`query/tools.py:_structure_summary` 는 걸러냈지만 `wiki_out/okf_writer.py` 는 걸르지 않아
|
||||
`x-calls` 40건이 거의 전부 ALV 상수와 `CALL SCREEN` 의 화면번호(`"100"`)였다.
|
||||
|
||||
- `parser/refs.py:EXTERNAL_CALL_KINDS` / `FUNCTION_CALL_KINDS` 로 기준을 한 곳에 두고
|
||||
요약·위키·엔티티 페이지가 공유한다.
|
||||
- ZFIR10070 의 `x-calls`: ALV 상수 40건 → **실제 FM·외부 PERFORM·SUBMIT 14건**.
|
||||
|
||||
### 설치 명령이 동작하지 않았음
|
||||
|
||||
README 가 안내하는 `pip install -e ".[dev]"` 가 실패했다 — flat-layout 자동 탐색이
|
||||
`data/` `wiki/` 까지 패키지로 오인한다. `pyproject.toml` 에 `[tool.setuptools] packages` 를 명시했다.
|
||||
|
||||
### 로더 강제 재적재
|
||||
|
||||
파서·색인 로직이 바뀌면 소스가 그대로라도 산출물이 달라지는데, `source_hash` 가 같으면 스킵돼
|
||||
반영할 방법이 없었다. `python -m index.loader --force` 를 추가했다.
|
||||
|
||||
## 7번 (정답셋)을 제외한 결과
|
||||
|
||||
1·4번의 효과를 숫자로 확인할 수단이 없다. 확인한 것은 **기능이 동작한다는 사실**뿐이다:
|
||||
|
||||
- `ACDOCT` / `FAGLL03` 로 질의 → 총계정원장 프로그램이 `동의어 확장` 으로 나온다
|
||||
- `"회계 담당자가 월 단위로 확인"`(요약 본문) → 프로그램이 나온다
|
||||
- `"계정코드를 입력하세요"`(텍스트 심볼) → 프로그램이 나온다
|
||||
|
||||
정밀도가 좋아졌는지, 아래 계수들이 적절한지, 2단 검색이 베이스라인보다 나은지는
|
||||
**측정하지 않았다**. 현 정답셋 5문항으로는 개선도 회귀도 보이지 않는다.
|
||||
|
||||
정답셋 50~100문항이 생긴 뒤에 튜닝할 값:
|
||||
|
||||
| 값 | 위치 | 현재 | 뜻 |
|
||||
|---|---|---|---|
|
||||
| `EXPANDED_WEIGHT` | `query/expand.py` | 0.35 | 동의어 히트 점수 감쇠 |
|
||||
| `MAX_SYNONYMS` | `query/expand.py` | 24 | 질의당 동의어 상한 |
|
||||
| `_DESC_WEIGHT` | `query/tools.py` | 0.6 | 요약·키워드 색인 히트 가중 |
|
||||
| `_BM25_WEIGHTS` | `index/db.py` | 표 참조 | FTS 컬럼별 bm25 가중치 |
|
||||
| 2단 검색 보충 조건 | `query/tools.py:_fts_rows` | `len(결과) < limit` | 동의어 단계 발동 시점 |
|
||||
|
||||
**이번 회귀가 이 과제의 필요성을 그대로 보여준다.** `find` recall@5 가 1.0 → 0.833 으로 떨어진 것을
|
||||
5문항 정답셋이 잡아냈지만, 원인 파악은 수동 디버깅으로 했다. 문항이 50개면 어떤 유형의 질의가
|
||||
깨졌는지가 바로 드러난다. 위 5개 값을 감으로 정한 상태가 현재의 가장 큰 미확정 요소다.
|
||||
Reference in New Issue
Block a user