AI 개발 · 심화
AI 에이전트 활용과 하네스 설계
맥락 엔지니어링 - 고르기·압축·메모·격리
맥락은 한정된 예산이라는 관점, 필요한 정보만 고르기, 긴 기록을 요약으로 압축, 맥락 밖 메모 파일에 적고 필요할 때 읽기, 하위 작업을 따로 돌려 요약만 받기(격리), 대표 예시 몇 개가 긴 규칙 목록보다 나은 이유, 완성 코드는 글자 수 예산을 둔 맥락 조립기를 만들고 네 기법을 적용한 전후 크기를 출력한다
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서 작업 목록과 상태 파일로 해야 할 일과 끝낸 일을 구분했다. 이제 하네스는 다음 행동을 결정할 때 어떤 정보를 모델에 보여 줄지 정해야 한다. 상태 파일을 만들었다고 해서 그 내용을 매번 모두 읽어 주어야 하는 것은 아니다. 현재 작업과 관계없는 기록까지 붙이면 중요한 조건을 찾기 어려워지고, 입력을 준비하는 비용도 늘어난다.
맥락(context)은 모델이 이번 판단에 사용할 수 있도록 전달받은 정보다. 작업 요청, 도구 결과, 대화 기록, 참고 자료가 여기에 들어간다. 이 장에서는 맥락을 한정된 예산으로 보고, 다음 판단에 필요한 근거를 남기는 방법을 구현한다. 실제 모델 대신 규칙 기반 가짜 모델을 사용하므로 같은 입력으로 같은 결과를 얻는다.
- 전체 자료와 이번 호출에 넣을 맥락을 구분하고 글자 수 예산을 적용한다.
- 현재 작업과 관련된 자료를 고르고 긴 기록을 판단에 필요한 요약으로 압축한다.
- 메모를 맥락 밖 파일에 보관한 뒤 필요한 부분만 읽는다.
- 하위 작업의 상세 기록을 격리하고 결과 요약만 상위 판단에 전달한다.
- 대표 예시를 포함한 조립 결과를 실행과 검증으로 확인한다.
문제 상황
문서 정리 하네스가 메모 문서를 보관 위치로 옮길지 판단한다고 하자. 작업 목록에는 사진 정리와 임시 파일 점검도 들어 있다. 이전 실행 기록에는 메모 문서를 확인한 과정이 여러 번 남아 있고, 별도 메모에는 보관 정책과 과거 논의가 함께 적혀 있다. 중복 점검 도구는 파일마다 확인 내용을 길게 반환한다.
가장 간단한 구현은 이 자료를 모두 이어 붙이는 것이다. 그러나 메모 문서의 다음 행동을 정하는 데 사진 정리 기록은 필요하지 않다. 이전 확인 과정 전체보다 최종 확인 결과가 더 중요하다. 중복 점검의 상세 출력도 중복 개수가 없다는 결론만 있으면 충분할 수 있다. 자료가 존재한다는 사실과 이번 판단에 필요하다는 사실은 다르다.
이때 단순히 앞부분부터 일정 길이만 남기면 문제가 생긴다. 보관 정책이 뒤쪽에 있으면 정책이 빠진 채 행동이 결정된다. 반대로 최신 기록만 남기면 처음에 정한 제약을 잃을 수 있다. 길이를 줄이는 일보다 어떤 정보가 빠져도 되는지 정하는 일이 먼저다.
이 장의 실습에서는 보관 정책이 유지로 설정되어 있고, 이전 확인 결과도 유지이며, 중복 개수는 0일 때 보관 행동을 선택한다. 실제 문서를 이동하지는 않는다. 다음 행동을 정하는 입력을 조립하고, 네 가지 기법을 적용하기 전후의 크기와 판단 결과를 확인하는 데 집중한다.
맥락을 예산으로 다루기
맥락 창(context window)은 한 번의 판단에 담을 수 있는 정보의 범위다. 실제 모델은 대체로 토큰(token)을 기준으로 입력과 출력의 한도를 계산한다. 토큰 수는 글자 수와 같지 않으며, 언어와 문자열 형태에 따라 달라진다. 여기서는 외부 패키지 없이 원리를 확인하기 위해 Python의 len()으로 글자 수를 센다. 이 수치를 특정 모델의 입력 한도로 해석해서는 안 된다.
예산은 자료를 다 모은 뒤 초과 여부를 확인하는 숫자만은 아니다. 무엇을 먼저 넣고 무엇을 포기할지 정하는 기준이기도 하다. 현재 작업의 대상, 적용할 정책, 확인 결과처럼 행동을 바꾸는 정보는 필수로 둔다. 대표 예시처럼 이해를 돕는 정보는 선택 항목으로 둔다. 필수 항목이 예산을 넘으면 호출을 멈추고 입력 구성을 다시 설계해야 한다.
완성 코드에서는 필수 항목을 모두 조립한 뒤 선택 항목을 순서대로 넣는다. 남은 자리에 들어가는 항목만 추가하며, 항목 내부를 자르지 않는다. 단어가 잘리거나 정책의 조건만 남는 일을 피하기 위한 작은 규칙이다. 이 방식은 가장 좋은 조합을 찾아 주는 최적화 알고리즘은 아니다. 선택 항목의 순서가 우선순위라는 단순한 계약을 사용한다.
| 정보 | 이 실습의 내용 | 예산 처리 | 빠졌을 때의 문제 |
|---|---|---|---|
| 현재 작업 | 메모 문서를 보관한다 | 필수 | 대상과 행동을 정할 수 없다 |
| 정책과 확인 결과 | 유지 정책, 유지 확인 | 필수 | 행동의 근거가 사라진다 |
| 하위 작업 결론 | 중복 개수 0 | 필수 | 보관 조건을 확인할 수 없다 |
| 대표 예시 | 유지와 정보 누락 사례 | 선택 | 해석 보조 자료가 줄어든다 |
글자 수를 세는 범위도 분명해야 한다. 이 프로그램은 블록 내용과 블록 사이의 줄바꿈을 센다. 제목이나 별도 요청문은 조립하지 않는다. 실제 하네스에서 역할 설명과 도구 설명까지 전달한다면 그것도 예산에 포함해야 한다. 보이는 자료만 세고 부가 문자열을 제외하면 측정값과 실제 전달량이 달라진다.
입력이 작다고 판단이 좋아졌다고 단정할 수는 없다. 정책 한 줄을 지워 크기를 줄였지만 행동이 바뀌었다면 실패다. 따라서 이 장은 크기를 측정하는 동시에 필요한 사실이 남았는지 검사한다. 작은 맥락과 올바른 맥락은 함께 확인해야 하는 서로 다른 조건이다.
고르기·압축·메모·격리
고르기는 관련성을 판단하는 일이다
고르기는 현재 작업에 필요한 자료를 선택하는 단계다. 실습의 자료는 메모, 사진, 임시 파일이라는 주제 필드를 갖는다. 현재 대상이 메모라면 주제가 일치하는 자료만 고른다. 자료 안에서도 행동에 필요한 사실과 확인 과정의 부가 기록을 구분해 필요한 필드만 가져온다.
주제 필드는 작은 예제에서 쓰기 좋은 선택 기준이다. 실제 자료에서는 파일 경로, 작업 식별자, 문서 종류처럼 출처가 분명한 기준을 함께 사용할 수 있다. 여기서 중요한 것은 길이가 짧다는 이유로 자료를 고르지 않는 것이다. 짧은 사진 기록보다 긴 메모 정책이 현재 판단에 더 필요할 수 있다.
선택은 정보의 경계를 정하는 작업이므로 놓친 자료가 없는지도 확인해야 한다. 완성 코드의 정책은 선택된 작업 자료와 메모 파일 양쪽에 나타난다. 가짜 모델은 양쪽 값이 일치할 때만 행동한다. 서로 다른 정책이 들어오면 먼저 등장한 값을 조용히 채택하지 않고 보류한다.
압축은 결론과 그 조건을 남기는 일이다
압축은 필요한 자료의 표현을 줄이는 단계다. 실습에서는 같은 대상을 확인한 기록이 열 개 있고 결과는 모두 유지다. 요약 함수는 모든 기록이 메모 문서에 관한 것인지, 결과가 하나로 일치하는지 확인한 뒤 최종 결과 한 줄을 만든다. 서로 다른 결과가 섞이면 요약을 만들지 않는다.
이 예제의 요약이 짧을 수 있는 이유는 상황이 단순하기 때문이다. 확인 결과가 같은 주제에 속하고 시간에 따른 정책 변경도 없다. 실제 기록에서 대상이나 조건이 다르면 하나의 결론으로 합치기 전에 구분해야 한다. 취소된 결정을 현재 결정처럼 요약하거나, 미확인을 성공으로 바꾸면 길이는 줄어도 의미가 달라진다.
요약은 원본을 대신하는 새로운 자료다. 그러므로 원본을 지우기보다 다시 확인할 수 있게 보관하는 편이 좋다. 이 프로그램에서는 원본 기록을 목록에 유지한다. 요약 결과가 의심스러우면 목록을 다시 검사할 수 있다. 압축의 목적은 이번 입력을 줄이는 것이지 근거를 없애는 것이 아니다.
메모와 격리는 자료가 머무는 위치를 바꾼다
메모는 이후에도 필요한 정보를 현재 맥락 밖에 적어 두는 방법이다. 메모 파일이 있다는 사실만으로 모델이 그 내용을 아는 것은 아니다. 하네스가 필요한 시점에 읽어서 전달해야 한다. 실습에서는 임시 폴더에 정책과 긴 부가 메모를 쓰고, 이번 판단에 필요한 정책 줄만 읽는다.
이때 파일 전체를 매번 읽어 그대로 붙이면 입력 크기는 줄지 않는다. 실습의 읽기 함수는 파일을 줄 단위로 살피고 정책 항목만 반환한다. 정책이 없거나 여러 개이면 실패한다. 같은 키가 여러 번 등장할 때 마지막 값을 임의로 고르는 대신, 메모를 정리해야 한다는 신호를 만든다.
격리는 하위 작업이 사용하는 상세 자료를 상위 판단에서 분리하는 방법이다. 중복 점검은 상세 스캔 기록을 다룰 수 있지만, 상위 작업에는 중복 개수만 돌려준다. 상위 맥락에 상세 기록을 복사하지 않으므로 그 기록이 커져도 상위 입력의 크기는 그대로다.
이 실습에서는 별도 함수의 지역 변수로 하위 작업을 흉내 낸다. 별도 프로세스나 다른 모델을 실행하지 않는다. 따라서 운영체제 수준의 접근 제한을 제공하는 격리는 아니다. 여기서의 경계는 어떤 자료를 상위 맥락에 전달하느냐에 관한 것이다. 하위 함수가 상세 문자열을 반환하기 시작하면 그 경계는 다시 사라진다.
네 기법은 함께 사용할 수 있지만 역할은 다르다. 고르기는 자료의 관련성을 정하고, 압축은 표현의 길이를 줄이며, 메모는 지속적으로 쓸 정보를 밖에 보관한다. 격리는 하위 작업의 상세 정보가 상위 작업에 섞이는 범위를 제한한다. 파일에 저장했다는 이유만으로 압축된 것도 아니고, 요약했다는 이유만으로 다른 작업에서 분리된 것도 아니다.
대표 예시로 판단의 경계 보여 주기
긴 규칙 목록은 조건을 빠짐없이 적는 데 도움이 된다. 그러나 추상적인 문장만 이어지면 실제 입력에서 어떤 결과를 골라야 하는지 해석할 일이 남는다. 대표 예시는 입력 상태와 기대 행동을 함께 보여 주므로 그 해석을 줄일 수 있다. 유지가 확인된 사례와 필요한 정보가 누락된 사례를 나란히 두면 행동과 보류의 경계를 짧게 전달한다.
유지가 확인되고 나머지 조건도 충족되면 보관한다.
필요한 정보가 없으면 멈추고 확인한다.
예시는 전체 정책을 대신하지 않는다. 위 문장에서 말하는 나머지 조건은 별도로 전달되어야 한다. 유지라는 단어만 보고 언제나 보관하도록 가르치면 중복 점검이 빠진 경우를 처리하지 못한다. 따라서 이 실습은 대상과 정책, 확인 결과, 중복 개수를 필수로 남기고 예시는 선택 항목으로 둔다.
좋은 예시는 서로 다른 판단을 대표한다. 성공 사례만 여러 개 늘리는 것보다 정보가 누락되어 보류하는 사례 하나를 추가하는 편이 경계를 보여 주기 쉽다. 다만 실제로 더 잘 작동하는지는 실행 결과로 확인해야 한다. 이 프로그램의 가짜 모델은 예시에서 학습하지 않으며 필수 필드를 검사한다. 예시가 주는 효과 자체를 실험하는 코드는 아니다.
완성 코드에서는 긴 규칙 자리의 문자열을 두 예시로 교체해 크기 차이도 출력한다. 반복된 부가 기록과 규칙 문자열은 크기를 재현하기 위한 합성 자료다. 줄어든 비율을 실제 프로젝트의 개선율로 해석하지 않는다. 무엇을 덜 전달했는지 단계별로 드러내는 측정 도구로 사용한다.
완성 코드
다음 내용을 main.py로 저장한다. Python 3.12 이상에서 표준 라이브러리만 사용하며 네트워크를 호출하지 않는다. 프로그램이 만드는 파일은 임시 폴더 안의 메모 파일 하나이며, 실행이 끝나면 폴더와 함께 정리된다. ASCII 필드 이름은 가짜 모델이 조건을 정확하게 검사하도록 정한 예제 내부의 표현이다.
from pathlib import Path
from tempfile import TemporaryDirectory
BUDGET = 120
def make_documents():
facts = "target=notes\naction=archive\npolicy=keep"
documents = []
for topic, core in (
("notes", facts),
("photos", "target=photos"),
("cache", "target=cache"),
):
documents.append({
"topic": topic,
"facts": core,
"raw": core + "\n" + "detail " * 80,
})
return documents
def make_history():
return [
{
"target": "notes",
"result": "keep",
"raw": (
f"step={index:02d}; checked=notes; result=keep\n"
+ "trace " * 20
),
}
for index in range(10)
]
def select_facts(documents, target):
selected = [
document["facts"]
for document in documents
if document["topic"] == target
]
if len(selected) != 1:
raise ValueError("작업 자료가 하나로 정해지지 않는다")
return selected[0]
def summarize_history(history, target):
if not history:
raise ValueError("확인 기록이 없다")
if any(item["target"] != target for item in history):
raise ValueError("다른 대상의 기록이 섞여 있다")
results = {item["result"] for item in history}
if results != {"keep"}:
raise ValueError("확인 결과가 유지로 일치하지 않는다")
return "result=keep"
def read_policy(path):
policies = []
with path.open(encoding="utf-8") as stream:
for line in stream:
value = line.rstrip("\n")
if value.startswith("policy="):
policies.append(value)
if len(policies) != 1:
raise ValueError("정책 항목은 하나여야 한다")
return policies[0]
def isolated_scan():
detail = "duplicates=0\n" + "scan " * 140
conclusion = detail.splitlines()[0]
return conclusion
def assemble(required, optional, budget):
context = "\n".join(required)
if len(context) > budget:
raise ValueError("필수 맥락이 예산을 넘는다")
included = 0
for block in optional:
candidate = "\n".join([context, block]) if context else block
if len(candidate) <= budget:
context = candidate
included += 1
return context, included
def fake_model(context):
fields = {}
for line in context.splitlines():
if "=" not in line or "=>" in line:
continue
key, value = line.split("=", 1)
fields.setdefault(key, set()).add(value)
expected = {
"target": {"notes"},
"action": {"archive"},
"policy": {"keep"},
"result": {"keep"},
"duplicates": {"0"},
}
if all(fields.get(key) == value for key, value in expected.items()):
return {"action": "archive", "target": "notes"}
return {"action": "hold", "target": "notes"}
def main():
documents = make_documents()
history = make_history()
memo_raw = "policy=keep\n" + "memo " * 100
scan_raw = "duplicates=0\n" + "scan " * 140
long_rules = "rule " * 100
blocks = [
"\n".join(document["raw"] for document in documents),
"\n".join(item["raw"] for item in history),
memo_raw,
scan_raw,
long_rules,
]
measurements = [("적용 전", len("\n".join(blocks)))]
blocks[0] = select_facts(documents, "notes")
measurements.append(("고르기 후", len("\n".join(blocks))))
blocks[1] = summarize_history(history, "notes")
measurements.append(("압축 후", len("\n".join(blocks))))
with TemporaryDirectory() as folder:
memo_path = Path(folder) / "memory.txt"
memo_path.write_text(memo_raw, encoding="utf-8")
blocks[2] = read_policy(memo_path)
measurements.append(("메모 후", len("\n".join(blocks))))
blocks[3] = isolated_scan()
measurements.append(("격리 후", len("\n".join(blocks))))
examples = "keep => archive\nmissing => stop"
context, included = assemble(blocks[:4], [examples], BUDGET)
assert len(context) == 108
assert len(context) <= BUDGET
assert included == 1
assert fake_model(context) == {
"action": "archive",
"target": "notes",
}
assert fake_model(context + "\npolicy=delete")["action"] == "hold"
assert fake_model(
context.replace("duplicates=0", "duplicates=1")
)["action"] == "hold"
required_size = len("\n".join(blocks[:4]))
compact, omitted = assemble(blocks[:4], [examples], required_size)
assert len(compact) == 76
assert omitted == 0
assert fake_model(compact)["action"] == "archive"
try:
assemble(blocks[:4], [], required_size - 1)
except ValueError:
pass
else:
raise AssertionError("필수 정보 초과를 감지하지 못했다")
result = fake_model(context)
for label, size in measurements:
print(f"{label}: {size}자")
print(f"예시 교체와 조립 후: {len(context)}자")
print(f"예산: {BUDGET}자")
print(f"선택 예시 블록: {included}개")
print(f"결정: {result['action']} {result['target']}")
print("검증: 통과")
if __name__ == "__main__":
main()
줄별 해설
첫 두 줄은 경로를 다루는 Path와 임시 폴더를 만드는 TemporaryDirectory를 가져온다. BUDGET은 최종 맥락의 글자 수 한도다. 함수가 전역 한도를 몰라도 검사할 수 있도록 assemble()에는 예산을 인자로 전달한다. 다른 작업에 다른 한도를 적용하려면 호출부만 바꾸면 된다.
make_documents()는 세 주제의 자료를 만든다. facts는 판단에 쓰는 구조화된 사실이고 raw는 사실에 긴 부가 기록을 붙인 원본 표현이다. 실제 자료에서 구조화된 필드를 얻는 과정은 별도로 필요하지만, 이 예제는 두 표현을 함께 만들어 선택 원리를 분리해서 보여 준다. 반복 문자열은 현실의 문서 내용을 흉내 내는 합성 자료다.
make_history()는 확인 기록 열 개를 만든다. 각 딕셔너리에는 대상, 결과, 상세 문자열이 있다. 상세 문자열의 순번은 고정되므로 실행할 때마다 길이가 같다. 요약 함수는 문자열에서 그럴듯한 결론을 추측하지 않고 구조화된 대상과 결과를 검사한다.
select_facts()의 목록 내포는 대상 주제가 일치하는 자료만 남긴다. 선택 결과가 없거나 여러 개이면 예외를 발생시킨다. 이 계약은 여러 정책 문서를 합치는 상황까지 처리하지 않는다. 자료가 여러 개일 때 어느 문서를 우선할지 정하지 않은 채 첫 문서를 반환하는 일을 방지한다.
summarize_history()는 빈 기록과 다른 대상의 혼입을 먼저 검사한다. 이어서 결과를 집합으로 모아 유지 하나만 남는지 확인한다. 이 검사를 통과했을 때에만 짧은 결과 문자열을 반환한다. 검사하지 않고 마지막 기록만 사용하는 구현과 달리, 이 예제의 요약은 기록 전체가 같은 결론을 지지해야 한다.
read_policy()는 파일을 한 줄씩 읽는다. 정책 줄만 목록에 모으며 정책이 하나일 때 반환한다. 이 함수는 파일에서 필요한 정보를 찾는 읽기 동작을 보여 준다. 파일이 매우 커지면 별도 색인 등이 필요할 수 있지만, 여기서는 파일 저장과 입력 전달이 서로 다른 동작임을 확인하면 된다.
isolated_scan()의 detail은 함수 안에서만 사용하는 상세 결과다. 반환되는 값은 첫 줄의 중복 개수다. 앞서 만든 scan_raw는 비교 측정을 위한 동일한 합성 원본이며 최종 맥락에는 들어가지 않는다. 하위 작업이 읽는 자료와 상위 판단이 받는 결과를 구분하는 것이 이 함수의 핵심이다.
assemble()은 필수 문자열을 줄바꿈으로 연결하고 크기를 검사한다. 선택 항목을 붙일 때마다 실제 후보 문자열을 만들어 길이를 다시 센다. 이 때문에 구분자의 길이도 자연스럽게 계산된다. 필수 초과에는 예외가 발생하고, 선택 항목 초과에는 해당 항목만 빠진다.
fake_model()은 각 키의 값을 집합으로 보관한다. 정책이 두 번 나타나도 둘 다 유지라면 집합에는 유지 하나만 남는다. 유지와 삭제가 함께 나타나면 기대 집합과 달라져 보류한다. 딕셔너리에 마지막 값만 저장하는 구현에서는 놓칠 수 있는 충돌을 이 방식으로 확인한다.
main()의 measurements는 네 기법을 적용할 때마다 전체 길이를 기록한다. 마지막 단계는 긴 규칙 블록을 예시 블록으로 바꾸고 예산에 맞춰 조립한다. 반복 정책을 중복 제거하지 않았으므로 최종 맥락에는 같은 정책 줄이 두 번 들어간다. 이 실습에서는 두 출처의 일치를 확인하기 위해 그대로 둔다.
assert는 크기뿐 아니라 판단 결과도 검사한다. 정책이 충돌하거나 중복 개수가 바뀌면 보류해야 한다. 예시가 들어갈 공간이 없을 때에도 필수 정보만으로 같은 행동이 나오는지 확인한다. 필수 크기보다 한 글자 작은 예산에서는 예외가 발생해야 한다. 검증용 assert를 실행하려면 최적화 옵션 없이 프로그램을 실행한다.
실행 결과
먼저 경고를 오류로 취급하여 컴파일을 확인하고 프로그램을 실행한다. py_compile 명령은 구문과 컴파일 시점의 경고를 확인하며 성공하면 출력하지 않는다. 실행 중의 assert는 조립 결과와 보류 조건을 확인한다. 컴파일 과정에서 생기는 캐시 파일을 포함해 장의 파일 작업을 임시 폴더 안에서만 수행하도록 다음 명령은 복사본을 만들어 실행한다. 명령은 main.py가 있는 폴더에서 실행한다.
python3 - <<'PY'
from pathlib import Path
from tempfile import TemporaryDirectory
import subprocess
import sys
source = Path("main.py").read_text(encoding="utf-8")
with TemporaryDirectory() as folder:
program = Path(folder) / "main.py"
program.write_text(source, encoding="utf-8")
subprocess.run(
[sys.executable, "-W", "error", "-m", "py_compile", str(program)],
check=True,
)
subprocess.run(
[sys.executable, "main.py"],
cwd=folder,
check=True,
)
PY
일반 실행 명령은 다음과 같다. 프로그램 자체의 파일 읽기와 쓰기는 모두 내부에서 만든 임시 폴더에서 이루어진다.
python3 main.py
두 실행 방법의 프로그램 출력은 같다.
적용 전: 5047자
고르기 후: 3337자
압축 후: 1779자
메모 후: 1278자
격리 후: 577자
예시 교체와 조립 후: 108자
예산: 120자
선택 예시 블록: 1개
결정: archive notes
검증: 통과
처음 네 번의 감소는 관련 자료 선택, 기록 요약, 정책만 읽기, 하위 작업 요약 전달에 해당한다. 마지막 감소는 500자의 합성 규칙 블록을 31자의 예시 블록으로 교체한 결과다. 숫자는 줄바꿈까지 포함한 정확한 문자열 길이이며 파일의 바이트 크기는 아니다.
AI가 제안한 구현을 채택할 때는 이 출력만 확인하고 끝내지 않는다. 변경 전후 차이(diff)에서 필수 항목 목록, 정책 충돌 처리, 하위 함수의 반환값을 살펴본다. 길이가 줄었어도 상세 결과를 잘못 해석하거나 예산 초과를 숨기는 변경이 섞일 수 있다. 실행 검증과 코드 검토는 서로 다른 종류의 실수를 발견한다.
실무에서 자주 틀리는 것
초과한 맥락을 뒤에서 자르기
문자열을 자르면 마지막 필수 항목이 일부만 남을 수 있다. 다음 코드는 실행되지만 중복 확인 항목을 손상한다. 예산을 맞췄다는 사실만으로 입력이 유효해지지는 않는다.
required = ["policy=keep", "duplicates=0"]
context = "\n".join(required)[:20]
고친 코드는 필수 정보를 통째로 남길 수 없으면 실패한다. 이후 호출부에서 자료 구성을 바꾸거나 예산을 조정해야 한다.
required = ["policy=keep", "duplicates=0"]
budget = 20
context = "\n".join(required)
if len(context) > budget:
raise ValueError("필수 정보를 모두 담을 수 없다")
긴 기록의 마지막 값을 요약으로 사용하기
마지막 값만 반환하면 기록 안의 의견 차이가 사라진다. 순서가 최신성을 뜻한다는 계약도 없다면 마지막이라는 이유만으로 신뢰할 근거가 없다.
results = ["hold", "keep"]
summary = results[-1]
고친 코드는 서로 다른 값이 있으면 재확인이 필요하다는 결론을 만든다. 실제로 최신 결정이 우선인 정책을 사용한다면 그 정책과 시점 정보도 함께 다뤄야 한다.
results = ["hold", "keep"]
unique_results = set(results)
summary = results[0] if len(unique_results) == 1 else "review"
assert summary == "review"
메모 파일 전체를 매번 전달하기
저장 위치만 파일로 옮기고 전체 내용을 계속 전달하면 맥락 예산은 줄지 않는다. 다음 코드는 정책 한 줄을 찾기 위해 부가 메모까지 포함한다.
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as folder:
path = Path(folder) / "memory.txt"
path.write_text("policy=keep\nold discussion\n", encoding="utf-8")
context = path.read_text(encoding="utf-8")
고친 코드는 정책 줄만 가져오고 개수를 확인한다. 파일 밖에 보관하는 단계와 필요한 정보만 읽는 단계가 함께 있어야 입력이 줄어든다.
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as folder:
path = Path(folder) / "memory.txt"
path.write_text("policy=keep\nold discussion\n", encoding="utf-8")
with path.open(encoding="utf-8") as stream:
policies = [
line.rstrip("\n")
for line in stream
if line.startswith("policy=")
]
if len(policies) != 1:
raise ValueError("정책을 하나로 확인해야 한다")
context = policies[0]
하위 작업의 상세 기록을 다시 붙이기
하위 작업을 별도 함수로 옮겼어도 상세 기록을 반환하면 상위 맥락은 계속 커진다. 함수의 위치보다 반환하는 정보의 범위가 중요하다.
def scan():
return "duplicates=0\n" + "scan detail\n" * 100
context = scan()
고친 코드는 상위 판단에 필요한 결론만 반환한다. 나중에 상세 근거를 조사할 필요가 생기면 그때 별도로 읽을 수 있게 설계한다.
def scan():
detail = "duplicates=0\n" + "scan detail\n" * 100
return detail.splitlines()[0]
context = scan()
assert context == "duplicates=0"
한눈에 보기
| 방법 | 핵심 질문 | 실습의 처리 | 확인할 점 |
|---|---|---|---|
| 고르기 | 이번 작업과 관계있는가 | 메모 자료의 사실만 선택 | 필요한 출처를 놓치지 않았는가 |
| 압축 | 어떤 결론과 조건을 남길까 | 확인 기록을 결과 한 줄로 요약 | 불일치와 미확인이 사라지지 않았는가 |
| 메모 | 밖에 보관하고 무엇을 읽을까 | 파일에서 정책만 읽기 | 필요할 때 실제로 읽는가 |
| 격리 | 상위 판단이 알아야 할 것은 무엇인가 | 중복 개수만 반환 | 상세 기록이 다시 섞이지 않는가 |
| 대표 예시 | 어떤 판단 경계를 보여 줄까 | 행동과 정보 누락 사례 전달 | 필수 정책을 예시로 대체하지 않았는가 |
맥락 조립기는 자료 저장소와 모델 호출 사이에 놓인다. 저장소는 나중에 다시 확인할 근거를 보관하고, 조립기는 현재 판단에 필요한 표현을 만든다. 둘을 구분하면 자료가 늘어날 때마다 입력도 같은 크기로 늘어나는 구조를 피할 수 있다. 다음 장에서 명세와 작업 자료를 연결할 때에도 어떤 근거를 이번 판단에 전달할지 정하는 기준이 필요하다.
연습 문제
- BUDGET을 76으로 바꾼다. 선택 예시가 빠지더라도 필수 정보와 행동 결과가 유지되도록 검증문을 고친다. 어떤 출력이 달라지는지 설명한다.
- 확인 기록 하나의 result를 hold로 바꾼다. 요약 함수가 왜 예외를 발생시키는지 설명하고, 이 상태를 유지로 요약하면 무엇을 잃는지 적는다.
- 메모 파일에 policy=delete 줄을 추가한다. read_policy()와 fake_model()이 각각 어느 경계에서 충돌을 발견하는지 설명한다.
- 하위 점검이 중복 2개를 발견한 상황을 만든다. 측정용 원본과 하위 함수의 결론을 함께 바꾸고, 최종 행동이 보류인지 검증한다.
정답과 해설
첫 문제에서 필수 맥락은 76자다. 예시를 추가하려면 줄바꿈 한 글자와 예시 31자가 더 필요하므로 예시는 빠진다. 최종 크기 검증을 76으로, included 검증을 0으로 바꾼다. 출력에서는 최종 크기가 76자, 예산이 76자, 선택 예시 블록이 0개가 된다. 필수 필드가 남아 있으므로 결정과 검증 결과는 유지된다. 앞 단계의 측정값은 변하지 않는다.
둘째 문제에서 결과 집합은 keep와 hold를 함께 포함한다. summarize_history()는 유지 하나로 일치하는 경우만 허용하므로 예외를 발생시킨다. 이를 유지로 요약하면 보류 의견이 있었다는 사실을 잃는다. 요약을 계속 만들려면 충돌을 명시하는 새로운 표현과 그 표현을 받았을 때의 행동을 함께 설계해야 한다.
셋째 문제에서 read_policy()는 정책 줄이 두 개라는 사실을 파일 읽기 단계에서 발견한다. 정책이 같은 값으로 중복되어도 이 함수의 계약상 실패다. 반면 fake_model()은 조립된 맥락에서 같은 키에 서로 다른 값이 들어왔는지 검사한다. 두 위치에서 서로 다른 종류의 불일치를 확인하므로 한 검사만으로 다른 검사를 대신할 수 없다.
넷째 문제에서는 scan_raw의 첫 줄과 isolated_scan() 내부의 첫 줄을 duplicates=2로 바꾼다. 원래의 보관 검증은 보류 검증으로 바꾸고, 필수 맥락만 전달하는 검증도 보류를 기대하도록 수정한다. 한 자리 숫자를 다른 한 자리 숫자로 바꾸었으므로 크기는 그대로다. 이 결과는 맥락의 크기가 같아도 내용에 따라 행동이 달라져야 한다는 사실을 보여 준다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.