View a markdown version of this page

LangGraph - Amazon Bedrock AgentCore

LangGraph

이 페이지에서는 LangGraph 에이전트를 계측하는 방법, 스팬을 식별하는 방법, 평가 필드를 추출하는 방법을 설명합니다. LangGraph 에이전트를 안정적으로 평가할 수 있도록 구성하는 모범 사례와 함께 종료됩니다.

주제

에이전트 계측

OpenTelemetry() 또는 OpenInferenceOpenInference()의 두 가지 계측 라이브러리 중 하나를 사용하여 LangGraph 에이전트를 계측할 수 있습니다openinference-instrumentation-langchain.opentelemetry-instrumentation-langchain Amazon Bedrock AgentCore Evaluations는 두 라이브러리를 모두 지원합니다. 라이브러리는 서로 다른 범위 이름을 내보내고 서로 다른 스팬 속성을 사용합니다. 평가 서비스는 각각에서 동일한 값을 추출합니다.

Amazon Bedrock AgentCore 런타임에서와 같이 에이전트가 AWS Distro for OpenTelemetry(ADOT)를 사용하여 실행되는 경우 명시적 계측 코드를 추가할 필요가 없습니다. 프로젝트의 종속성에 계측 라이브러리를 추가하면 충분합니다. ADOT는 시작 시 이를 검색하고 자동으로 활성화합니다.

종속성에 원하는 경로의 계측 라이브러리를 추가합니다. 다음 예제에서는 최소 버전을 고정하고 고정할 이유가 없는 한 사용 가능한 최신 버전을 사용합니다.

OpenTelemetry

참고: 버전 0.55.0 이상을 사용합니다. 버전 0.55.0에는 평가 서비스가 의존하는 최신 OpenTelemetry 생성형 AI 에이전트 스팬 규칙에 대한 지원이 추가되었습니다.

opentelemetry-instrumentation-langchain을 종속성에 추가합니다. 방출되는 범위 이름은 입니다opentelemetry.instrumentation.langchain.

requirements.txt:

opentelemetry-instrumentation-langchain>=0.55.0

pyproject.toml:

[project] dependencies = [ "opentelemetry-instrumentation-langchain>=0.55.0", ]
OpenInference

openinference-instrumentation-langchain을 종속성에 추가합니다. 방출되는 범위 이름은 입니다openinference.instrumentation.langchain.

requirements.txt:

openinference-instrumentation-langchain>=0.1.62

pyproject.toml:

[project] dependencies = [ "openinference-instrumentation-langchain>=0.1.62", ]
참고

계측은 관찰성을 설정하는 한 단계입니다. 평가를 위해 원격 측정을 내보내려면 관찰성 설정에서 전체 설정을 완료합니다.

스팬 식별 방법

스팬을 분류하는 데 사용되는 속성은 두 계측 라이브러리 간에 다릅니다.

OpenTelemetry

OpenTelemetry 계측 라이브러리는 traceloop.span.kind 속성을 사용하여 스팬을 분류하고 최신 버전도를 설정합니다gen_ai.operation.name.

범위 유형 속성 식별

에이전트 호출

traceloop.span.kind = workflow (또한 gen_ai.operation.name = invoke_agent)

도구 실행

traceloop.span.kind = tool (또한 gen_ai.operation.name = execute_tool)

Inference

gen_ai.operation.name = chat

OpenInference

OpenInference 계측 라이브러리는 openinference.span.kind 속성을 사용하여 스팬을 분류합니다.

범위 유형 속성 식별

에이전트 호출

openinference.span.kind = CHAIN 또는 AGENT

도구 실행

openinference.span.kind = TOOL

Inference

openinference.span.kind = LLM

평가 필드 추출 방법

호출 에이전트 범위의 경우 입력 및 출력에 메시지별 목록 정리가 포함되지 않습니다. 대신 콘텐츠는 직렬화된 LangChain 그래프 상태, 즉 전체 상태를 래핑하는 JSON 문자열입니다. 이 직렬화된 상태의 정확한 모양은 두 계측 라이브러리 간에 다릅니다. 두 경우 모두 서비스는 이를 구문 분석하여 사용자 프롬프트(사람 메시지)와 에이전트 응답(AI 메시지)을 찾습니다.

또한 LangGraph는 둘 이상의 형식으로 메시지 역할을 직렬화합니다. 역할은 소문자 값(human, ai, tool) 또는 LangChain 메시지 클래스 이름(HumanMessage, AIMessage, )으로 표시될 수 있습니다ToolMessage. 서비스는 두 양식을 모두 인식합니다.

이 콘텐츠의 위치는 원격 측정이 수집된 방식에 따라 달라집니다. 식별 속성(traceloop.span.kind 또는 openinference.span.kind)은 두 경우 모두 스팬에 있습니다. 자세한 내용은 스팬, 이벤트 레코드 및 원격 측정 신호를 참조하세요.

이벤트 레코드에서

원격 측정이 분할되면 서비스는 각 범위와 상관관계가 있는 이벤트 레코드에서 콘텐츠를 읽습니다.

  • 사용자 프롬프트에이전트 응답: body.input 및의 에이전트 스팬 호출 이벤트 레코드에서body.output.

  • 도구 호출: 실행 도구 범위의 도구 이름입니다. 도구 인수와 결과는 body.input 및의 해당 스팬의 이벤트 레코드에서 가져옵니다body.output.

예제는 이벤트 레코드가 있는 스팬 예제를 참조하세요.

스팬 속성에서

원격 측정이 분할되지 않으면 속성과 동일한 콘텐츠가 스팬에 유지됩니다. 속성은 계측 라이브러리에 따라 다릅니다.

  • OpenTelemetry:

    • 사용자 프롬프트에이전트 응답: 에이전트 간접 호출 스팬에서 gen_ai.task.inputgen_ai.task.output에서.

    • 도구 호출: gen_ai.tool.call.result실행 도구 범위의의 도구 이름과 및의 gen_ai.tool.name인수 gen_ai.tool.call.arguments 및 결과입니다.

  • OpenInference:

    • 사용자 프롬프트에이전트 응답: 에이전트 간접 호출 스팬에서 input.valueoutput.value에서.

    • 도구 호출: output.value실행 도구 범위의의 도구 이름과 및의 tool.name인수 input.value 및 결과입니다.

예제는 이벤트 레코드가 없는 스팬 예제를 참조하세요.

이벤트 레코드가 있는 스팬의 예

원격 측정이 분할되면 스팬은 식별 속성을 전달하고 콘텐츠는 상관관계가 있는 이벤트 레코드에 있습니다. 다음 예제는 Amazon Bedrock AgentCore 런타임에 배포된 LangGraph 여행 계획 에이전트에서 가져온 것입니다. 각 계측 라이브러리 아래에 동일한 에이전트가 표시됩니다.

참고

이 예제는 완전한 범위가 아닙니다. 실제 에이전트 상호 작용의 대표 데이터를 표시하며, 일부 필드는 생략되고 긴 값은 가독성을 위해 잘립니다.

OpenTelemetry

Invoke agent span

traceloop.span.kind 속성(workflow)은 이를 호출 에이전트 범위로 식별합니다. 최신 라이브러리 버전도 gen_ai.operation.name = 로 설정됩니다invoke_agent.

{ "traceId": "6a01eef11066751d68f90def0da1f80a", "spanId": "ba1833fa7f097041", "parentSpanId": "836a5ccf9a2186cc", "name": "travel_agent.workflow", "kind": "INTERNAL", "scope": { "name": "opentelemetry.instrumentation.langchain", "version": "0.60.0" }, "startTimeUnixNano": 1778511607308521744, "endTimeUnixNano": 1778511610930280395, "durationNano": 3621758651, "attributes": { "traceloop.span.kind": "workflow", "gen_ai.operation.name": "invoke_agent", "gen_ai.agent.name": "travel_agent", "gen_ai.provider.name": "langgraph", "traceloop.workflow.name": "travel_agent", "session.id": "sea-nyc-trip-2-turns-adot_v17_opentelemetry_0_60_0" }, "status": { "code": "OK" } }

상관관계가 있는 이벤트 레코드에는 대화가 포함됩니다. 각 메시지의 content는 직렬화된 LangChain 그래프 상태입니다. 입력은 inputs 키 아래에 상태를 래핑합니다. 출력은 각 메시지를 LangChain 생성자 객체로 사용하여 outputs 키 아래에 이를 래핑합니다. 사용자 프롬프트는 인적 메시지이고 에이전트 응답은 직렬화된 상태 내의 AI 메시지입니다.

{ "spanId": "ba1833fa7f097041", "traceId": "6a01eef11066751d68f90def0da1f80a", "scope": { "name": "opentelemetry.instrumentation.langchain" }, "body": { "input": { "messages": [ { "content": "{\"inputs\": {\"messages\": [{\"role\": \"user\", \"content\": \"Hey, how can you help me\"}]}, \"tags\": [], \"metadata\": {\"ls_integration\": \"langchain_create_agent\", \"lc_agent_name\": \"travel_agent\", \"thread_id\": \"sea-nyc-trip-2-turns-adot_v17_opentelemetry_0_60_0\"}, \"kwargs\": {\"name\": \"travel_agent\"}}", "role": "user" } ] }, "output": { "messages": [ { "content": "{\"outputs\": {\"messages\": [{\"lc\": 1, \"type\": \"constructor\", \"id\": [\"langchain\", \"schema\", \"messages\", \"HumanMessage\"], \"kwargs\": {\"content\": \"Hey, how can you help me\", \"type\": \"human\", \"id\": \"12345678-1234-1234-1234-123456789012\"}}, {\"lc\": 1, \"type\": \"constructor\", \"id\": [\"langchain\", \"schema\", \"messages\", \"AIMessage\"], \"kwargs\": {\"content\": \"Hello! I'm your travel planning assistant ...\", \"type\": \"ai\"}}]}, \"kwargs\": {\"tags\": []}}", "role": "assistant" } ] } } }
Execute tool span

traceloop.span.kind 속성(tool)은 이를 실행 도구 범위로 식별합니다. 에는 도구 이름과 gen_ai.operation.name =가 gen_ai.tool.name 들어 있습니다execute_tool.

{ "traceId": "6a01eefa5c52f3d86a35038f35f5ba30", "spanId": "5b332f3cd15ace04", "parentSpanId": "922a21edc04eba29", "name": "execute_tool search_flights", "kind": "INTERNAL", "scope": { "name": "opentelemetry.instrumentation.langchain", "version": "0.60.0" }, "startTimeUnixNano": 1778511614892698232, "endTimeUnixNano": 1778511614893399618, "durationNano": 701386, "attributes": { "traceloop.span.kind": "tool", "gen_ai.operation.name": "execute_tool", "gen_ai.tool.name": "search_flights", "gen_ai.tool.type": "function", "gen_ai.tool.description": "Search for available flights between cities.", "gen_ai.provider.name": "langgraph", "traceloop.workflow.name": "travel_agent", "session.id": "sea-nyc-trip-2-turns-adot_v17_opentelemetry_0_60_0" }, "status": { "code": "OK" } }

상관관계가 있는 이벤트 레코드에는 도구 입력( 인수) 및 출력(결과, LangChain으로 직렬화됨)이 포함됩니다ToolMessage.

{ "spanId": "5b332f3cd15ace04", "traceId": "6a01eefa5c52f3d86a35038f35f5ba30", "scope": { "name": "opentelemetry.instrumentation.langchain" }, "body": { "input": { "messages": [ { "content": "{\"origin\": \"SEA\", \"destination\": \"NYC\", \"date\": \"2025-03-15\"}" } ] }, "output": { "messages": [ { "role": "tool", "name": "search_flights", "content": "{\"origin\": \"SEA\", \"destination\": \"NYC\", \"flights\": [ ... ]}" } ] } } }

OpenInference

OpenInference 라이브러리를 사용하면 스팬 유형이 openinference.span.kind 속성에 전달되고 에이전트 입력 및 출력이 상관관계가 있는 이벤트 레코드에 직렬화됩니다.

Invoke agent span

openinference.span.kind 속성( CHAIN또는 그래프가 이름으로 컴파일된 AGENT 경우)은 이를 호출 에이전트 범위로 식별합니다.

{ "traceId": "6a387ee61078243c1cc455ed45c6c313", "spanId": "0a7990d804132a9b", "parentSpanId": "29ae22014173881c", "name": "LangGraph", "kind": "INTERNAL", "scope": { "name": "openinference.instrumentation.langchain", "version": "0.1.66" }, "startTimeUnixNano": 1782087405949310976, "endTimeUnixNano": 1782087408945828864, "durationNano": 2996517888, "attributes": { "openinference.span.kind": "CHAIN", "input.mime_type": "application/json", "output.mime_type": "application/json", "llm.input_messages.0.message.role": "user", "session.id": "sea-nyc-trip-2-turns-oi-0-1-66" }, "status": { "code": "OK" } }

상관관계가 있는 이벤트 레코드에는 대화가 포함됩니다. 사용자 프롬프트는 사람-역할 메시지이고 에이전트 응답은 직렬화된 메시지의 AI-역할 메시지입니다.

{ "spanId": "0a7990d804132a9b", "traceId": "6a387ee61078243c1cc455ed45c6c313", "scope": { "name": "openinference.instrumentation.langchain" }, "body": { "input": { "messages": [ { "role": "user", "content": "{\"messages\": [{\"role\": \"user\", \"content\": \"Hey, how can you help me\"}]}" } ] }, "output": { "messages": [ { "content": "{\"messages\": [{\"type\": \"human\", \"data\": {\"content\": \"Hey, how can you help me\", ...}}, {\"type\": \"ai\", \"data\": {\"content\": \"Hello! I'm your travel planning assistant ...\", ...}}]}", "role": "assistant" } ] } } }
Execute tool span

openinference.span.kind 속성(TOOL)은 이를 실행 도구 범위로 식별합니다.는 도구 이름을 tool.name 보유합니다.

{ "traceId": "6a387ef07b8f4f3732fab45d3c0b51ff", "spanId": "ab105c12cc40048f", "parentSpanId": "9b2d4e72760690b4", "name": "search_flights", "kind": "INTERNAL", "scope": { "name": "openinference.instrumentation.langchain", "version": "0.1.66" }, "startTimeUnixNano": 1782087411724620032, "endTimeUnixNano": 1782087411725306880, "durationNano": 686848, "attributes": { "openinference.span.kind": "TOOL", "tool.name": "search_flights", "tool.description": "Search for available flights between cities.", "input.mime_type": "application/json", "output.mime_type": "application/json", "session.id": "sea-nyc-trip-2-turns-oi-0-1-66" }, "status": { "code": "OK" } }

상관관계가 있는 이벤트 레코드에는 도구 입력( 인수) 및 출력(결과, LangChain으로 직렬화됨)이 포함됩니다ToolMessage.

{ "spanId": "ab105c12cc40048f", "traceId": "6a387ef07b8f4f3732fab45d3c0b51ff", "scope": { "name": "openinference.instrumentation.langchain" }, "body": { "input": { "messages": [ { "role": "user", "content": "{\"origin\": \"SEA\", \"destination\": \"NYC\", \"date\": \"2025-03-15\"}" } ] }, "output": { "messages": [ { "content": "{\"type\": \"tool\", \"data\": {\"content\": \"{\\\"origin\\\": \\\"SEA\\\", \\\"destination\\\": \\\"NYC\\\", \\\"flights\\\": [ ... ]}\", \"type\": \"tool\", \"name\": \"search_flights\", \"tool_call_id\": \"toolu_bdrk_01LzXXJCfpfuS7Bpf7e1qLMg\", \"status\": \"success\"}}", "role": "assistant" } ] } } }

이벤트 레코드가 없는 스팬의 예

원격 측정이 분할되지 않으면 동일한 콘텐츠가 스팬 속성에 유지되고 별도의 이벤트 레코드가 생성되지 않습니다. 다음은 LangGraph 여행 계획 에이전트의 예입니다. 각 계측 라이브러리 아래에 동일한 에이전트가 표시됩니다.

참고

이 예제는 완전한 범위가 아닙니다. 실제 에이전트 상호 작용의 대표 데이터를 표시하며, 일부 필드는 생략되고 긴 값은 가독성을 위해 잘립니다.

OpenTelemetry

Invoke agent span

gen_ai.task.input 속성은 사용자 프롬프트를 유지하고 gen_ai.task.output 속성은 에이전트 응답과 함께 직렬화된 상태를 유지합니다. 둘 다 직렬화된 LangChain 그래프 상태입니다.

{ "traceId": "6a4de7b85e61747e6b568a1f4768e89d", "spanId": "31ea3d5882dac680", "name": "LangGraph.workflow", "kind": "INTERNAL", "scope": { "name": "opentelemetry.instrumentation.langchain", "version": "0.62.1" }, "attributes": { "traceloop.span.kind": "workflow", "gen_ai.operation.name": "invoke_agent", "gen_ai.agent.name": "LangGraph", "gen_ai.task.input": "{\"inputs\": {\"messages\": [{\"role\": \"user\", \"content\": \"Hey, how can you help me\"}]}, \"tags\": [], \"metadata\": { ... }, \"kwargs\": {\"name\": \"LangGraph\"}}", "gen_ai.task.output": "{\"outputs\": {\"messages\": [{\"lc\": 1, \"type\": \"constructor\", \"id\": [\"langchain\", \"schema\", \"messages\", \"HumanMessage\"], \"kwargs\": {\"content\": \"Hey, how can you help me\", \"type\": \"human\"}}, {\"lc\": 1, \"type\": \"constructor\", \"id\": [\"langchain\", \"schema\", \"messages\", \"AIMessage\"], \"kwargs\": {\"content\": \"Hello! I'm your travel planning assistant ...\", \"type\": \"ai\"}}]}, \"kwargs\": {\"tags\": []}}", "session.id": "sea-nyc-trip-2-turns-unified" }, "status": { "code": "OK" } }
Execute tool span

gen_ai.tool.call.arguments 속성에는 도구 인수가 있고 gen_ai.tool.call.result 속성에는 LangChain 로 직렬화된 도구 결과가 있습니다ToolMessage.

{ "traceId": "6a4de7c376913db82e6f0f336a16731d", "spanId": "b64c37adefae74f0", "name": "execute_tool search_flights", "kind": "INTERNAL", "scope": { "name": "opentelemetry.instrumentation.langchain", "version": "0.62.1" }, "attributes": { "traceloop.span.kind": "tool", "gen_ai.operation.name": "execute_tool", "gen_ai.tool.name": "search_flights", "gen_ai.tool.description": "Search for available flights between cities.", "gen_ai.tool.call.arguments": "{\"input_str\": \"{'origin': 'SEA', 'destination': 'NYC', 'date': '2025-03-15'}\", \"inputs\": {\"origin\": \"SEA\", \"destination\": \"NYC\", \"date\": \"2025-03-15\"}, \"metadata\": { ... }}", "gen_ai.tool.call.result": "{\"output\": {\"lc\": 1, \"type\": \"constructor\", \"id\": [\"langchain\", \"schema\", \"messages\", \"ToolMessage\"], \"kwargs\": {\"content\": \"{\\\"origin\\\": \\\"SEA\\\", \\\"destination\\\": \\\"NYC\\\", \\\"flights\\\": [ ... ]}\", \"type\": \"tool\", \"name\": \"search_flights\", \"status\": \"success\"}}}", "session.id": "sea-nyc-trip-2-turns-unified" }, "status": { "code": "OK" } }

OpenInference

Invoke agent span

input.value 속성은 사용자 프롬프트를 유지하고 output.value 속성은 에이전트 응답과 함께 직렬화된 상태를 유지합니다.

{ "traceId": "6a387ee61078243c1cc455ed45c6c313", "spanId": "b8c0b67876b78b91", "name": "LangGraph", "kind": "INTERNAL", "scope": { "name": "openinference.instrumentation.langchain", "version": "0.1.66" }, "attributes": { "openinference.span.kind": "CHAIN", "input.value": "{\"messages\": [{\"role\": \"user\", \"content\": \"Hey, how can you help me\"}]}", "output.value": "{\"messages\": [{\"type\": \"human\", \"data\": {\"content\": \"Hey, how can you help me\"}}, {\"type\": \"ai\", \"data\": {\"content\": \"Hello! I'm your travel planning assistant ...\"}}]}", "session.id": "sea-nyc-trip-2-turns-oi-0-1-66" }, "status": { "code": "OK" } }
Execute tool span

input.value 속성에는 도구 인수가 있고 output.value 속성에는 LangChain 로 직렬화된 도구 결과가 있습니다ToolMessage.

{ "traceId": "6a387ef07b8f4f3732fab45d3c0b51ff", "spanId": "58752612d9b22ae1", "name": "search_flights", "kind": "INTERNAL", "scope": { "name": "openinference.instrumentation.langchain", "version": "0.1.66" }, "attributes": { "openinference.span.kind": "TOOL", "tool.name": "search_flights", "tool.description": "Search for available flights between cities.", "input.value": "{\"origin\": \"SEA\", \"destination\": \"NYC\", \"date\": \"2025-03-15\"}", "output.value": "{\"type\": \"tool\", \"data\": {\"content\": \"{\\\"origin\\\": \\\"SEA\\\", \\\"destination\\\": \\\"NYC\\\", \\\"flights\\\": [ ... ]}\", \"name\": \"search_flights\"}}", "session.id": "sea-nyc-trip-2-turns-oi-0-1-66" }, "status": { "code": "OK" } }

LangGraph 에이전트 모범 사례

LangGraph 에이전트를 빌드하고 호출하는 방법은 원격 측정에 표시되는 내용과 에이전트를 얼마나 안정적으로 평가할 수 있는지에 영향을 줍니다. 다음 방법은 사용자 프롬프트, 에이전트 응답 및 도구 활동을 복구할 수 있도록 하는 데 도움이 됩니다.

1. 에이전트 구성 패턴 선택

LangGraph 에이전트를 빌드하는 두 가지 일반적인 방법이 있습니다.

  • 사전 구축create_agent: 시작하는 가장 빠른 방법입니다. LangGraph의 기본 제공 실행 루프를 통해 전달되는 대화를 통해 턴당 단일 호출 에이전트 스팬을 생성합니다. 사용자 지정 제어 흐름이 없는 표준 reason-act 에이전트를 원할 때 사용합니다.

    from langchain.agents import create_agent agent = create_agent(model=model, tools=[search_flights, book_flight])
  • 사용자 지정 StateGraph : 노드, 엣지 및 조건부 라우팅을 완벽하게 제어할 수 있습니다. 각 노드 실행은 자체 범위가 되므로 추적이 더 세분화됩니다. 사용자 지정 오케스트레이션이 필요할 때 사용합니다.

    from langgraph.graph import StateGraph, START, END from typing_extensions import TypedDict class State(TypedDict): messages: list graph = StateGraph(State) graph.add_node("generate_response", generate_response) graph.add_node("tools", run_tools) graph.add_edge(START, "generate_response") agent = graph.compile()

두 패턴 모두 동일한 방식으로 평가됩니다. 차이점은 트레이스의 세분성입니다.

2. messages 그래프 상태에서 사용(권장)

평가 서비스는 에이전트의 입력 및 출력 메시지에서 대화를 재구성합니다. messages 필드를 사용하는 것은 필수는 아니지만 가장 안정적인 추출이 가능합니다. 사용자 지정의 경우 상태의 messages 필드에 대화를 StateGraph유지합니다.

  • messages를 상태에 포함합니다(권장). 다른 사용자 지정 필드(예: user_id 또는 메타데이터)를 추가할 수 있습니다. messages이 있으면 표준 추출에서 사용자 프롬프트와 에이전트 응답을 직접 찾습니다. messages이 없는 경우 서비스는 개별 추론 범위에서 대화를 재구성하는 것으로 돌아가며, 이는 신뢰성이 떨어집니다.

  • 대체하지 말고 추가하세요. 전체 대화 기록이 보존되도록 새 메시지를 덮어쓰지 않고 목록에 추가하는 LangGraph 규칙을 따릅니다.

  • 표준 LangChain 메시지 유형(, , , )을 사용합니다.HumanMessage AIMessage ToolMessage SystemMessage 계측은 이를 올바르게 직렬화하고 서비스는 해당 역할을 인식합니다.

3. 지원되는 형식으로 사용자 메시지 전달

LangGraph 에이전트를 호출할 때 그래프의 messages 상태에 사용자 메시지를 추가합니다. LangGraph는 세 가지 교체 가능한 형식으로 메시지를 수락하며 AgentCore 평가는 모든 메시지를 지원합니다. 각는 서비스가 읽을 수 있는 스팬 및 이벤트 레코드를 생성합니다.

  • 튜플: (role, content) 페어:

    agent.invoke({"messages": [("user", user_message)]}, config=config)
  • LangChain 메시지 객체: aHumanMessage(또는 기타 메시지 클래스):

    from langchain_core.messages import HumanMessage agent.invoke({"messages": [HumanMessage(content=user_message)]}, config=config)
  • 사전: {"role", "content"} 사전:

    agent.invoke({"messages": [{"role": "user", "content": user_message}]}, config=config)

세 가지 형식 모두 동일한 messages 상태가 되므로 어떤 형식을 선택하든 사용자 프롬프트와 에이전트 응답이 동일하게 추출됩니다.