create_agent(..., middleware=[...]) 가 만드는 루프(모델 호출 ↔ 도구 실행)에서 before_model / after_model / wrap_model_call / wrap_tool_call 이 각각 어디에 끼는지, 질문 하나가 지나가는 순서대로 보여 줍니다.
탭을 바꾸면 훅 관찰, 여러 개를 끼웠을 때의 순서, 로깅(읽기만)과 마스킹(State 고치기), 요약 미들웨어가 메시지를 줄이는 시점을 차례로 볼 수 있습니다. 모델이 만든 값은 (예시) 로 표시했습니다.
| 훅 | 그래프 안의 모양 | 불리는 때 | 돌려주는 값의 뜻 | 이 날의 예 |
|---|---|---|---|---|
before_model(state, runtime) | 노드 이름.before_model | 모델을 부르기 직전. 질문마다가 아니라 모델 호출마다 | None = State 그대로, dict = State 변경분 | HookTrace [1], Logging, Masking, Summarization |
after_model(state, runtime) | 노드 이름.after_model | 모델 응답(AIMessage)이 State 에 붙은 직후 | 위와 같음 | HookTrace [2], Logging |
wrap_model_call(request, handler) | 노드가 아님. model 노드 안에서 호출을 감쌈 | 모델 호출 하나하나 | handler(request) 를 불러야 모델이 호출됨. 안 부르고 다른 응답을 돌려주면 호출 생략 | 이 묶음에는 없음 (guards.py 의 입력 가드) |
wrap_tool_call(request, handler) | 노드가 아님. tools 노드 안에서 도구 실행을 감쌈 | 도구 호출 하나하나 | handler(request) 의 결과(ToolMessage)를 돌려줘야 함 | HookTrace [3][4] |
LoggingMiddleware 의 두 훅은 State 를 읽어 agent_log.jsonl 에 한 줄을 쓰고 None 을 돌려줍니다. 질문 하나에 도구를 한 번 쓰면 before / after 쌍이 두 번, 모두 네 줄이 쌓이고 messages 는 한 번도 바뀌지 않습니다.
MaskingMiddleware 는 {"messages": [HumanMessage(content=masked, id=last_human.id)]} 를 돌려줍니다. reducer add_messages 가 같은 id 를 보고 그 자리의 메시지를 교체하므로 개수는 그대로이고 원문은 State 에서 사라집니다. id 를 빼면 교체가 아니라 추가가 됩니다.
리스트가 [A, B, C] 일 때입니다. 원고와 설치된 langchain 1.4.7 의 create_agent 구현을 기준으로 했습니다. before 노드는 리스트 순서대로 엣지가 이어지고, after 노드는 model 다음에 리스트의 마지막 것부터 이어집니다.
| 훅 | 실행 순서 | 기억하는 법 |
|---|---|---|
before_model | A → B → C | 앞에 적은 것이 먼저 |
after_model | C → B → A (A 가 루프의 출구: 여기서 tools / END 가 갈림) | 들어간 순서의 반대로 나옴 |
wrap_model_call | A( B( C( 모델 호출 ) ) ) | 앞에 적은 것이 가장 바깥 껍질 (docstring: "first in list as outermost layer") |
wrap_tool_call | A( B( C( 도구 실행 ) ) ) | 같은 규칙 (docstring: "first = outermost") |
훅의 종류가 다르면 리스트 순서만으로 실제 순서를 알 수 없습니다. 예를 들어 리스트 맨 앞의 wrap_model_call 미들웨어도 다른 미들웨어들의 before_model 노드가 모두 끝난 뒤에야 돕니다. 이 함정은 5일차 종합 실습 플레이어에서 여섯 겹으로 다시 볼 수 있습니다.
ToolMessage 입니다. 로깅이 message_type 을 함께 남기는 이유입니다.search_employee 결과의 이메일이 그대로 agent_log.jsonl 에 남습니다. 마스킹은 가장 최근 HumanMessage 하나만 고치므로 순서를 바꿔도 이 줄은 가려지지 않습니다.id=last_human.id 를 빼면 가려진 메시지가 하나 더 붙을 뿐 원문이 남습니다.middleware_order.py 가 name 프로퍼티를 덮어쓴 이유입니다.hook_trace.py, middleware_order.py, summarization_demo.py 에는 if __name__ == "__main__": 이 없습니다. 반대로 logging_middleware.py, masking_middleware.py 는 실행 코드가 그 안에 있어 guards.py 가 클래스만 가져다 씁니다.hook_trace.py: 훅마다 print 를 넣어 [1] before_model, [2] after_model, [3][4] wrap_tool_call 의 순서를 관찰middleware_order.py: 이름표만 찍는 미들웨어 A, B, C 로 before / after 의 순서 관찰logging_middleware.py: 모델 호출 전후를 agent_log.jsonl 에 기록하는 LoggingMiddleware (읽기만)masking_middleware.py: 사용자 입력의 사번·전화번호·이메일을 같은 id 로 교체해 가리는 MaskingMiddleware (State 고치기)summarization_demo.py: 내장 SummarizationMiddleware 와 InMemorySaver 로 턴마다 메시지 개수 관찰