TECH NOTE · AI COLLAB
소스를 읽고 내린 판단과 실제로 해 본 결과가 갈렸습니다. 못 뗀다고 적어 둔 코드 둘이 떼어 보니 됐고, 그 차이가 어디서 생기는지 정리했습니다.
기획서에 "이건 못 뗀다"고 적어 뒀다
앱 코드에서 예제로 쓸 부분을 골라야 했다. 소스를 읽고 다섯 개를 뽑았는데, 그중 둘은 떼어내기 어렵다고 판단했다. 기획 문서에 이렇게 적었다.
A(창 위치 읽기)와 C(환청 필터)는 떼어내면 혼자 돌아간다. 폴더를 만들 값이 있다. 반면 B(프롬프트 예산)와 D(메모리 잠금)는 앱 구조에 묶여 있어서, 억지로 떼면 원본 맥락 없이는 못 읽는 조각이 된다.
근거가 있었다. build_prompt_with_budget은 PromptInput, BuiltPrompt, LocalAgentError 같은 타입을 받고 돌려주는데, 그 타입들이 또 다른 것을 참조한다. 잠금 쪽은 MemoryStore 안에 들어 있고 MemorySnapshot과 얽혀 있다.
읽어 보면 실이 여러 갈래로 뻗어 있는 게 보인다. 하나를 당기면 딸려 온다.
그런데 실제로 떼어 보니 둘 다 됐다.
프롬프트 예산
의존해 보이던 것들이 대부분 렌더링 때문이었다.
원본은 프롬프트를 JSON으로 직렬화하고, 마크업을 이스케이프하고, 응답 스키마를 붙인다. 그 과정에 타입이 여럿 필요하다.
하지만 이 코드의 요점은 그게 아니다. 예산이 모자랄 때 무엇부터 버리는가다.
if !turns.is_empty() {
turns.remove(0);
} else if examples.pop().is_some() {
...
} else if !persona.is_empty() {
persona = halve_or_clear(&persona);
} else {
return Err(BuildError::BudgetExceeded);
}
이 사슬은 자체 완결적이다. 렌더링을 문자열 조립으로 바꾸니 나머지가 전부 떨어져 나갔다. 남은 건 의존성 0개에 테스트 다섯 개다.
메모리 잠금
이쪽은 더 허무했다. 원래 독립적이었다.
pub(super) fn lock_exclusive(file: &File) -> Result<(), StoreError> {
if unsafe { flock(file.as_raw_fd(), LOCK_EX) } == 0 { ... }
}
flock 래퍼라 File 하나만 받는다. MemoryStore 안에 있다는 사실 때문에 얽혀 있다고 봤는데, 위치와 결합도는 다른 것이었다.
앱에는 잠금이 두 겹(프로세스 내 + OS 파일)인데 바깥쪽 한 겹만 남기니 그대로 돌았다. 안쪽은 설명으로 옮겼다.
왜 읽으면 얽혀 보이나
원인이 몇 가지 있었다.
코드를 읽으면 참조가 전부 보인다. 함수 시그니처에 타입 다섯 개가 나오면 다섯 개가 필요해 보인다. 실제로는 그중 셋이 반환값 포장이고 하나는 오류 종류라, 목적을 바꾸면 안 쓰는 것이었다.
목적이 바뀌면 필요한 것도 바뀐다. 앱에서는 프롬프트를 실제로 만들어야 하니 직렬화가 필수다. 예제에서는 순서만 보여주면 되니 필요 없다. 읽을 때는 원본의 목적으로 읽게 된다.
위치를 결합으로 착각한다. 같은 파일, 같은 모듈, 같은 구조체 안에 있으면 묶여 있다고 느낀다. 잠금 함수가 그랬다.
읽은 것과 해 본 것은 다르게 받아야 한다
이 일에서 얻은 건 “그때 판단이 틀렸다”가 아니다. 판단의 종류가 달랐다는 것이다.
기획 문서의 그 문장은 조사 결과처럼 적혀 있었다. 하지만 실은 읽고 내린 추정이었다. 확인한 것과 추정한 것이 같은 문단에 같은 어조로 섞여 있었다.
AI에게 조사를 시킬 때 특히 그렇다. 코드베이스를 훑어 달라고 하면 파일과 줄 수와 함수 이름이 정확한 요약이 온다. 그 안에 “이건 분리하기 어렵다” 같은 판단도 같은 확신으로 섞여 있다.
앞의 것은 읽으면 확인되는 사실이고 뒤의 것은 해 봐야 아는 것이다.
구분해서 받는 방법
이후로는 조사 결과를 이렇게 나눠 받는다.
| 종류 | 예 | 어떻게 받나 |
|---|---|---|
| 읽으면 확인되는 것 | 파일 줄 수, 함수 이름, 호출 관계 | 그대로 믿는다 |
| 실행하면 확인되는 것 | 테스트 통과, 실제 출력값 | 실행 결과를 함께 받는다 |
| 해 봐야 아는 것 | 분리 가능성, 성능, 난이도 | 추정이라고 표시하게 한다 |
세 번째를 그냥 두면 사실처럼 굳는다. 기획 문서에 한 줄로 적히고, 다음에 그 문서를 읽는 사람은 확인된 것으로 읽는다.
실제로 이번에도 그럴 뻔했다. 그 문장 때문에 두 편은 예제 없이 글만 쓸 계획이었다.
비용이 생각보다 싸다
떼어 보는 데 얼마나 걸리나 재 봤다. 두 예제 모두 한 시간이 안 걸렸다. 렌더링을 단순화하고 테스트를 붙이는 게 전부였다.
읽고 추정하는 데 든 시간과 큰 차이가 없다. 오히려 추정은 여러 번 다시 읽게 되는데, 해 보는 건 한 번에 답이 나온다.
“해 보면 알 수 있는데 시간이 오래 걸릴 것 같아서 추정한다”가 성립하는 경우가 생각보다 적다. 그 시간을 실제로 재 보지 않고 짐작하는 것 자체가 또 하나의 추정이다.
정리
- 코드를 읽으면 참조가 전부 보여서 실제보다 얽혀 보인다
- 목적이 바뀌면 필요한 의존도 바뀐다. 원본의 목적으로 읽으면 안 보인다
- 같은 파일에 있다는 것과 묶여 있다는 것은 다르다
- 조사 결과에서 읽어서 안 것과 해 봐야 아는 것을 갈라서 받는다
- 후자를 사실처럼 적어 두면 다음 사람이 확인된 것으로 읽는다
- 떼어 보는 비용을 재 보지 않고 짐작하는 것도 추정이다
문서에 그 문장을 남길 때 “읽고 판단했다”는 표시를 안 했다. 그게 이번의 진짜 실수다.