Open2

LLMはどれだけ人間の意図を汲み取ってクエリを生成してくれるのか

YutaYuta

概要

  • 京阪電車で、北浜駅から出町柳駅までの時刻をサクッと調べるAI
  • 時刻表アプリだと、出発駅や到着駅を調べる必要があるが、自分用にカスタマイズしとくとパパッと調べられて便利

内容

こんな感じの、京阪電車時刻表を作った。

出発時刻	到着時刻	種別
6:22	7:16	特急
6:35	7:28	特急
6:48	7:41	特急
7:01	7:54	特急
7:14	8:06	特急
7:25	8:18	特急
7:37	8:30	特急
7:48	8:42	特急
8:01	8:49	特急
8:15	8:56	特急
8:31	9:09	特急
8:42	9:36	特急
8:48	9:47	快速急行
9:01	9:54	特急
9:13	10:07	特急
9:24	10:11	快速特急
9:31	10:26	特急
9:43	10:39	特急
9:54	10:41	快速特急

コード

  • 一番シンプルなLangGraph
llm = ChatOpenAI(model="gpt-4o-mini")


class State(BaseModel):
    question: str = Field(..., description="ユーザーの質問")
    query: str = Field("", description="LLMによって生成されたSQLクエリ")
    result: str = Field("", description="SQLクエリの実行結果")
    answer: str = Field("", description="ユーザーの質問に対する回答")

class QueryOutput(BaseModel):
    """SQLクエリを返すLLMの出力"""
    query: str = Field(..., description="モデルによって生成されたSQLクエリ")

def write_query(state: State):
    """ユーザーの質問からSQLクエリを生成してstateに書き込む"""
    query_prompt_template = hub.pull("langchain-ai/sql-query-system-prompt")

    prompt = query_prompt_template.invoke(
        {
            "dialect": db.dialect,
            "top_k": 10,
            "table_info": db.get_table_info(),
            "input": state.question,
        }
    )
    structured_llm = llm.with_structured_output(QueryOutput)
    result = structured_llm.invoke(prompt)
    return {"query": result.query}

def write_result(state: State):
    """SQLを実行し、結果をstateに書き込む"""
    result = str(db.run(state.query)) # デフォルトだとinclude_columnsはFalseであり、結果は文字列で書かれたリストになる

    return {"result": result}


def write_answer(state: State):
    """resultから回答を生成してstateに書き込む"""
    template = """データ抽出結果から、ユーザーの質問に対する回答を生成してください。
    データ抽出結果: {result}
    ユーザーの質問: {question}
    """
    prompt = ChatPromptTemplate.from_template(template)
    chain = prompt | llm
    answer = chain.invoke({"result": state.result, "question": state.question})
    return {"answer":answer}

# ワークフローの定義
from langgraph.graph import StateGraph

workflow = StateGraph(State)

workflow.add_node(write_query)
workflow.add_node(write_result)
workflow.add_node(write_answer)

workflow.set_entry_point("write_query")
workflow.add_edge("write_query", "write_result")
workflow.add_edge("write_result", "write_answer")
workflow.set_finish_point("write_answer")

compiled = workflow.compile()

# 実行
initial_state = State(question="京阪電車で、8:00に出発します。一番早い電車は?")

result = compiled.invoke(initial_state)

print(result["answer"])

結果

「京阪電車のテーブルを参照する」「8:00に出発するのだから、8:00以降で最も早い電車を調べる」を汲み取り、クエリを作成してくれた

SELECT "出発時刻", "到着時刻", "種別" 
FROM public20250112."京阪電車_時刻表_土日" 
WHERE "出発時刻" > '08:00' 
ORDER BY "出発時刻" ASC 
LIMIT 1;

解答

content='京阪電車の一番早い特急は、8:01に出発します。到着は8:49です。'
YutaYuta

目的

  • RAGから情報を引っ張ってくるか、webから検索するかを、LLMに判断させたい

方法

  • Routeというクラスを作る。
  • RAGには、「ダンダダン」「スパイファミリー」「キングダム」の情報があると記述。
  • chainで、Routeクラスからrag_documentかwebを選ばせるようにする
route_prompt = ChatPromptTemplate.from_template("""\
 質問に回答するために適切なRetrieverを選択してください。

 質問: {question}
 """)

class Route(str,Enum):
    rag_document = "stories includes dandadan, spy-family, and kingdom"
    web = "web"

class RouteOutput(BaseModel):
    route: Route

model = ChatOpenAI(model="gpt-4o-mini")

route_chain = (
    route_prompt
    | model.with_structured_output(RouteOutput)
    | (lambda x: x.route)
)

res = route_chain.invoke("「スパイファミリー」について教えてください")

print(res)

結果

  • 上記を試すと、スパイファミリーやダンダダンはrag_documentから、チェーンソーマンはwebから、というようにルートを適切に判断できた。
  • 最初は`rag_document = "マンガ「ダンダダン」のストーリー"というように記述していたが、これではダンダダンを投げてもwebから検索しようとした。人間的にはこの書き方のほうが判断つきそうと思うが、意外とAI(gpt-4o-mini)は判断を誤ってしまう