langgraph / / 2024. 11. 17. 17:38

[langgraph] 프롬프트 생성기 (Prompt Generation from User Requirements)

langgraph의 공식문서를 번역해 놓은 자료입니다. 필요한 경우 부연 설명을 추가하였습니다. 문제가 되면 삭제하겠습니다.

https://langchain-ai.github.io/langgraph/tutorials/customer-support/customer-support/

이 예제에서는 사용자가 프롬프트를 생성하도록 도와주는 챗봇을 만들 것이다. 챗봇은 먼저 사용자로부터 요구 사항을 수집한 후, 프롬프트를 생성하고(사용자의 입력에 따라 이를 다듬는다). 이 과정은 두 개의 별도 상태로 나누어지며, LLM이 이들 간의 전환 시기를 결정한다.

시스템의 그래픽 표현은 아래에서 확인할 수 있다.

정보 수집

먼저, 사용자 요구 사항을 수집할 그래프의 부분을 정의한다. 이는 특정 시스템 메시지를 사용하는 LLM 호출이 될 것이다. 프롬프트를 생성할 준비가 되었을 때 호출할 수 있는 도구에 접근할 수 있다.

from typing import List

from langchain_core.messages import SystemMessage
from langchain_openai import ChatOpenAI

from pydantic import BaseModel
template = """Your job is to get information from a user about what type of prompt template they want to create.

You should get the following information from them:

- What the objective of the prompt is
- What variables will be passed into the prompt template
- Any constraints for what the output should NOT do
- Any requirements that the output MUST adhere to

If you are not able to discern this info, ask them to clarify! Do not attempt to wildly guess.

After you are able to discern all the information, call the relevant tool."""


def get_messages_info(messages):
    return [SystemMessage(content=template)] + messages


class PromptInstructions(BaseModel):
    """Instructions on how to prompt the LLM."""

    objective: str
    variables: List[str]
    constraints: List[str]
    requirements: List[str]


llm = ChatOpenAI(temperature=0)
llm_with_tool = llm.bind_tools([PromptInstructions])


def info_chain(state):
    messages = get_messages_info(state["messages"])
    response = llm_with_tool.invoke(messages)
    return {"messages": [response]}

Prompt 생성

이제 프롬프트를 생성할 상태를 설정한다. 이는 별도의 시스템 메시지와 함께, 도구 호출 이전의 모든 메시지를 필터링하는 함수가 필요하다(이 시점에서 이전 상태는 프롬프트를 생성할 시간이라고 판단했기 때문이다).

from langchain_core.messages import AIMessage, HumanMessage, ToolMessage

# SystemPrompt
prompt_system = """Based on the following requirements, write a good prompt template:

{reqs}"""


# 프롬프트의 메시지를 가져오는 함수
# 도구 호출 이후의 메시지만 가져온다.
def get_prompt_messages(messages: list):
    tool_call = None
    other_msgs = []
    for m in messages:
        if isinstance(m, AIMessage) and m.tool_calls:
            tool_call = m.tool_calls[0]["args"]
        elif isinstance(m, ToolMessage):
            continue
        elif tool_call is not None:
            other_msgs.append(m)
    return [SystemMessage(content=prompt_system.format(reqs=tool_call))] + other_msgs


def prompt_gen_chain(state):
    messages = get_prompt_messages(state["messages"])
    response = llm.invoke(messages)
    return {"messages": [response]}

상태(state) 로직 정의

챗봇이 어떤 상태에 있는지를 나타내는 로직이다. 마지막 메시지가 도구 호출이라면, 우리는 "프롬프트 생성기" (prompt)가 응답해야 하는 상태에 있다. 그렇지 않고 마지막 메시지가 HumanMessage가 아니라면, 다음에 사람이 응답해야 한다는 것을 알 수 있으므로 우리는 END 상태에 있다. 마지막 메시지가 HumanMessage인 경우, 이전에 도구 호출이 있었다면 우리는 프롬프트 상태에 있다. 그렇지 않다면, 우리는 "정보 수집" (info) 상태에 있다.

from typing import Literal

from langgraph.graph import END


def get_state(state):
    messages = state["messages"]
    if isinstance(messages[-1], AIMessage) and messages[-1].tool_calls:
        return "add_tool_message"
    elif not isinstance(messages[-1], HumanMessage):
        return END
    return "info"

Graph 정의

이제 그래프를 생성할 수 있다. 대화 기록을 지속하기 위해 MemorySaver를 사용할 것이다.

from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START
from langgraph.graph.message import add_messages
from typing import Annotated
from typing_extensions import TypedDict


class State(TypedDict):
    messages: Annotated[list, add_messages]


memory = MemorySaver()
workflow = StateGraph(State)
workflow.add_node("info", info_chain)
workflow.add_node("prompt", prompt_gen_chain)


@workflow.add_node
def add_tool_message(state: State):
    return {
        "messages": [
            ToolMessage(
                content="Prompt generated!",
                tool_call_id=state["messages"][-1].tool_calls[0]["id"],
            )
        ]
    }


workflow.add_conditional_edges("info", get_state, ["add_tool_message", "info", END])
workflow.add_edge("add_tool_message", "prompt")
workflow.add_edge("prompt", END)
workflow.add_edge(START, "info")
graph = workflow.compile(checkpointer=memory)
from IPython.display import Image, display

display(
    Image(
        graph.get_graph().draw_mermaid_png(
            output_file_path="./prompt_generation_from_user_requirements.png"
        )
    )
)

Graph 사용하기

이제 생성된 챗봇을 사용할 수 있다.

import uuid

cached_human_responses = ["hi!", "rag prompt", "1 rag, 2 none, 3 no, 4 no", "red", "q"]
cached_response_index = 0
config = {"configurable": {"thread_id": str(uuid.uuid4())}}
while True:
    try:
        user = input("User (q/Q to quit): ")
    except:
        user = cached_human_responses[cached_response_index]
        cached_response_index += 1
    print(f"User (q/Q to quit): {user}")
    if user in {"q", "Q"}:
        print("AI: Byebye")
        break
    output = None
    for output in graph.stream(
        {"messages": [HumanMessage(content=user)]}, config=config, stream_mode="updates"
    ):
        last_message = next(iter(output.values()))["messages"][-1]
        last_message.pretty_print()

    if output and "prompt" in output:
        print("Done!")
User (q/Q to quit): hi!
================================== Ai Message ==================================

Hello! How can I assist you today?
User (q/Q to quit): rag prompt
================================== Ai Message ==================================

Sure! I can help you create a prompt template. To get started, could you please provide me with the following information:

1. What is the objective of the prompt?
2. What variables will be passed into the prompt template?
3. Any constraints for what the output should NOT do?
4. Any requirements that the output MUST adhere to?

Once I have this information, I can assist you in creating the prompt template.
User (q/Q to quit): 1 rag, 2 none, 3 no, 4 no
================================== Ai Message ==================================
Tool Calls:
  PromptInstructions (call_tcz0foifsaGKPdZmsZxNnepl)
 Call ID: call_tcz0foifsaGKPdZmsZxNnepl
  Args:
    objective: rag
    variables: ['none']
    constraints: ['no']
    requirements: ['no']
================================= Tool Message =================================

Prompt generated!
================================== Ai Message ==================================

Please write a response using the RAG (Red, Amber, Green) rating system.
Done!
User (q/Q to quit): red
================================== Ai Message ==================================

Response: The status is RED.
User (q/Q to quit): q
AI: Byebye

상세 설명

현재 프롬프트를 좀 더 자세히 살펴보자.

위의 그래프를 다시 보자.

image-20241009160545658

info 노드

info 노드는 PromptInstructions라는 도구를 가지고 있는 노드이다.

사용자의 입력을 받아서 아래의 4가지 항목이 만족하는 경우에만 tool을 실행시키게 되어 있다. 모르면 추측하지 말고 사용자에게 물어보라고 되어 있다. (ask them to clarify!)

4가지 항목은 아래와 같다.

  • objective, variable, constraints, requirements

그래서 사용자 메시지에서 4가지 항목을 모두 입력하지 않으면 다시 물어본다.
만일 모든 정보가 다 입력이 되었다면 적절한 툴(PromptInstructions)을 호출한다. (call the relevant tool)

workflow.add_conditional_edges("info", get_state, ["add_tool_message", "info", END])

info 노드에 conditional_edge가 연결되어 있다. 이 edge에 연결된 정보에는 get_state를 보고 판단하는 로직이 들어가 있다.

def get_state(state):
    messages = state["messages"]
    if isinstance(messages[-1], AIMessage) and messages[-1].tool_calls:
        return "add_tool_message"
    elif not isinstance(messages[-1], HumanMessage):
        return END
    return "info"

마지막 메시지(messages[-1])가 AIMessage이고 tool_calls이 있으면 add_tool_message 노드를 호출한다. 이 말은 모든 입력항목(4가지)이 모두 입력이 되어서 tool 사용을 해서 프롬프트를 만들 준비가 되었다는 것을 의미한다. 그리고 HumanMessage이면 End를 호출하고 그 외는 info 노드를 호출한다.

prompt 노드

def prompt_gen_chain(state):
    messages = get_prompt_messages(state["messages"])
    response = llm.invoke(messages)
    return {"messages": [response]}

이 노드는 연결된 도구가 없다. 단지 주어진 프롬프트를 통해 llm을 호출한다. 프롬프트의 내용은 아래와 같다.

prompt_system = """Based on the following requirements, write a good prompt template:

{reqs}"""

prompt에서 다음 요구사항을 기반으로 프롬프트를 만들라고 되어 있는데 그 요구사항이 reqs로 들어간다.

아래 로직은 tool 메시지의 인수를 가져오는 함수이다.

def get_prompt_messages(messages: list):
    tool_call = None
    other_msgs = []
    for m in messages:
        if isinstance(m, AIMessage) and m.tool_calls:
            tool_call = m.tool_calls[0]["args"]
        elif isinstance(m, ToolMessage):
            continue
        elif tool_call is not None:
            other_msgs.append(m)
    return [SystemMessage(content=prompt_system.format(reqs=tool_call))] + other_msgs

메시지가 AIMessage이고 tool_calls가 있으면 tool_call에 인수를 저장한다.

여기에 messages는 아래와 같다.

[
    HumanMessage(content='프롬프트 만들려고 해', ...), 
    AIMessage(content='프롬프트를 만들기 위해 다음 정보를 알려주세요:\n\n1. 프롬프트의 목적은 무엇인가요?\n2. 프롬프트 템플릿에 전달될 변수는 무엇인가요?\n3. 출력물이 하면 안 되는 제약사항이 있나요?\n4. 출력물이 준수해야 하는 요구사항이 있나요?', ...), 
    HumanMessage(content='1: rag, 2: no, 3: no, 4: no', ...), 
    AIMessage(content='', ... tool_calls=[{'name': 'PromptInstructions', 'args': {'objective': 'rag', 'variables': ['no'], 'constraints': ['no'], 'requirements': ['no']}, 'id': 'call_ZYxZukQv4ysoT9qt4HaTjvJ3', 'type': 'tool_call'}], ...),
    ToolMessage(content='Prompt generated!', id='0d596aa9-8a2e-44b0-aebe-45dc08f8cf8c', tool_call_id='call_ZYxZukQv4ysoT9qt4HaTjvJ3')]

HumanMessage -> AIMessage -> HumanMessage -> AIMessage => ToolMessage 순으로 실행이 되는데 4번째 AIMessage에서 tool_calls의 args를 뽑아서 tool_call에 넣고 prompt 생성시 reqs에 넣어준다.

그래서 get_prompt_messages 함수에서 리턴되는 결과는 아래와 같다.

[SystemMessage(content="Based on the following requirements, write a good prompt template:\n\n{'objective': 'rag', 'variables': ['no'], 'constraints': ['no'], 'requirements': ['no']}", additional_kwargs={}, response_metadata={})]

이를 LLM에 던저서 결과를 가져온다.

실행을 해보면 gpt-4o-mini나 gpt-4o보다 gpt-3.5-turbo가 더 잘 동작하는 듯 하다.

실행

실제로 실행을 한번 해보자.

실행하기 전에 langchain.debug = True로 하고 로그를 확인하자.

[Human]

아래 메시지를 입력하자.

User (q/Q to quit): 프롬프트를 만들려고 해

[llm 응답]

[llm/start] [chain:LangGraph > chain:info > llm:ChatOpenAI] Entering LLM run with input:
{
  "prompts": [
    "System: Your job is to get information from a user about what type of prompt template they want to create.\n\nYou should get the following information from them:\n\n- What the objective of the prompt is\n- What variables will be passed into the prompt template\n- Any constraints for what the output should NOT do\n- Any requirements that the output MUST adhere to\n\nIf you are not able to discern this info, ask them to clarify! Do not attempt to wildly guess. Answer in Korean.\n\nAfter you are able to discern all the information, call the relevant tool.\nHuman: 프롬프트를 만들려고 해"
  ]
}
[llm/end] [chain:LangGraph > chain:info > llm:ChatOpenAI] [4.26s] Exiting LLM run with output:
{
  "generations": [
    [
      {
        "text": "프롬프트를 만들기 위해 몇 가지 정보를 알려주세요:\n\n1. 프롬프트의 목적은 무엇인가요?\n2. 프롬프트 템플릿에 전달될 변수는 무엇인가요?\n3. 출력이 하면 안 되는 제약 사항이 있나요?\n4. 출력이 준수해야 하는 요구 사항이 있나요?",
        "generation_info": {
          "finish_reason": "stop",
          "logprobs": null
        },
        "type": "ChatGeneration",
        "message": {
          ...
          "kwargs": {
            "content": "프롬프트를 만들기 위해 몇 가지 정보를 알려주세요:\n\n1. 프롬프트의 목적은 무엇인가요?\n2. 프롬프트 템플릿에 전달될 변수는 무엇인가요?\n3. 출력이 하면 안 되는 제약 사항이 있나요?\n4. 출력이 준수해야 하는 요구 사항이 있나요?",
            "additional_kwargs": {
              "refusal": null
            },
            "response_metadata": {
              ...
              },
              "model_name": "gpt-3.5-turbo-0125",
              "system_fingerprint": null,
              "finish_reason": "stop",
              "logprobs": null
            },
            "type": "ai",
            ...
            },
            "tool_calls": [],
            "invalid_tool_calls": []
          }
        }
      }
    ]
  ],
  "llm_output": {
    ...
  },
  ...
}

[AI]

프롬프트를 만들기 위해 몇 가지 정보를 알려주세요:

1. 프롬프트의 목적은 무엇인가요?
2. 프롬프트 템플릿에 전달될 변수는 무엇인가요?
3. 출력이 하면 안 되는 제약 사항이 있나요?
4. 출력이 준수해야 하는 요구 사항이 있나요?

[Human]

프롬프트의 목적만 rag로 입력하고 나머지는 모두 no라고 입력한다.

1: rag, 2: no, 3: no, 4: no

[llm 응답]

[llm/start] [chain:LangGraph > chain:info > llm:ChatOpenAI] Entering LLM run with input:
{
  "prompts": [
    "System: Your job is to get information from a user about what type of prompt template they want to create.\n\nYou should get the following information from them:\n\n- What the objective of the prompt is\n- What variables will be passed into the prompt template\n- Any constraints for what the output should NOT do\n- Any requirements that the output MUST adhere to\n\nIf you are not able to discern this info, ask them to clarify! Do not attempt to wildly guess. Answer in Korean.\n\nAfter you are able to discern all the information, call the relevant tool.\nHuman: 프롬프트를 만들려고 해\nAI: 프롬프트를 만들기 위해 몇 가지 정보를 알려주세요:\n\n1. 프롬프트의 목적은 무엇인가요?\n2. 프롬프트 템플릿에 전달될 변수는 무엇인가요?\n3. 출력이 하면 안 되는 제약 사항이 있나요?\n4. 출력이 준수해야 하는 요구 사항이 있나요?\nHuman: 1: rag, 2: no, 3: no, 4: no"
  ]
}
[llm/end] [chain:LangGraph > chain:info > llm:ChatOpenAI] [887ms] Exiting LLM run with output:
{
  "generations": [
    [
      {
        "text": "",
        "generation_info": {
          "finish_reason": "tool_calls",
          "logprobs": null
        },
        "type": "ChatGeneration",
        "message": {
          ...
          "kwargs": {
            "content": "",
            "additional_kwargs": {
              "tool_calls": [
                {
                  "id": "call_AGOU6EhCiO84ZgEEcP7EHgF8",
                  "function": {
                    "arguments": "{\"objective\":\"rag\",\"variables\":[\"no\"],\"constraints\":[\"no\"],\"requirements\":[\"no\"]}",
                    "name": "PromptInstructions"
                  },
                  "type": "function"
                }
              ],
              "refusal": null
            },
            "response_metadata": {
              ...
              },
              "model_name": "gpt-3.5-turbo-0125",
              "system_fingerprint": null,
              "finish_reason": "tool_calls",
              "logprobs": null
            },
            "type": "ai",
            "id": "run-43fdad64-b413-4ed3-aa89-96239c3021fb-0",
            "tool_calls": [
              {
                "name": "PromptInstructions",
                "args": {
                  "objective": "rag",
                  "variables": [
                    "no"
                  ],
                  "constraints": [
                    "no"
                  ],
                  "requirements": [
                    "no"
                  ]
                },
                "id": "call_AGOU6EhCiO84ZgEEcP7EHgF8",
                "type": "tool_call"
              }
            ],
            "usage_metadata": {
             ...
            },
            "invalid_tool_calls": []
          }
        }
      }
    ]
  ],
  "llm_output": {
    ...
    },
    "model_name": "gpt-3.5-turbo-0125",
    "system_fingerprint": null
  },
  "run": null,
  "type": "LLMResult"
}

프로프트 생성에 필요한 4가지 항목이 모두 입력이 되었으므로 PromptInstructions이라는 tool을 호출한다. tool 호출 시 필요한 인수가 모두 잘 호출된 것을 확인할 수 있다.

"arguments": "{\"objective\":\"rag\",\"variables\":[\"no\"],\"constraints\":[\"no\"],\"requirements\":[\"no\"]}",

그리고 prompt 노드가 실행이 되어 아래와 같이 Tool message가 실행이 된다.

[llm/start] [chain:LangGraph > chain:prompt > llm:ChatOpenAI] Entering LLM run with input:
{
  "prompts": [
    "System: Based on the following requirements, write a good prompt template:\n\n{'objective': 'rag', 'variables': ['no'], 'constraints': ['no'], 'requirements': ['no']}"
  ]
}
[llm/end] [chain:LangGraph > chain:prompt > llm:ChatOpenAI] [1.05s] Exiting LLM run with output:
{
  "generations": [
    [
      {
        "text": "Please write a response that discusses the advantages and disadvantages of the RAG (Red, Amber, Green) rating system without using any variables, constraints, or requirements.",
        "generation_info": {
          "finish_reason": "stop",
          "logprobs": null
        },
        "type": "ChatGeneration",
        "message": {
          ...
          "kwargs": {
            "content": "Please write a response that discusses the advantages and disadvantages of the RAG (Red, Amber, Green) rating system without using any variables, constraints, or requirements.",
            "additional_kwargs": {
              "refusal": null
            },
            "response_metadata": {
              ...
              },
              "model_name": "gpt-3.5-turbo-0125",
              "system_fingerprint": null,
              "finish_reason": "stop",
              "logprobs": null
            },
            "type": "ai",
            "id": "run-396733f3-62f9-4f5b-885b-e138e5ecf715-0",
            "usage_metadata": {
             ...
            },
            "tool_calls": [],
            "invalid_tool_calls": []
          }
        }
      }
    ]
  ],
  "llm_output": {
    ...
    },
    "model_name": "gpt-3.5-turbo-0125",
    "system_fingerprint": null
  },
  "run": null,
  "type": "LLMResult"
}

이와 같이 주어진 요구사항에 맞게 프롬프트가 생성되는 것을 확인할 수 있다.

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유