設計書をもとにしたDeepResearchもどきを作成してみた

# はじめに
会社の設計書を食わせて、RAGを作ってみたのですが、いまいち答えられない質問が多くて、AIエージェント開発をすれば解決できるのでは？という初心者発想で設計書を食わせたAIエージェント開発をしてみました。

# できたもの
※記載の情報はすべて架空の設計書情報です。（ChatGPTに架空の設計書を作成して、と伝えて作ってもらったもの）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/a701abd5-16b1-4a45-8be3-9d83088b1fea.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/ea32598b-3ad8-49c7-94ba-9a3292b4e570.png)

処理中は以下のような表示が出ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/adbfd4f7-778b-42e9-9b8c-38ea83112887.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/64827fd7-1075-4800-a42f-7f60fcb2c184.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/94da2e9c-603c-4c72-9adb-e715a4229a9b.png)



# 解説
構造は以下の通りです。
![pako_eNqdU02PmzAQ_SvW9JKVgBKWj8SscmmuvfVUISEvjMES2NSYbbNR_nsds5vSbNqV1hd7xu_NzHuyj1CpGoGC7_uFrJTkoqGFJIR36mfVMm1cREg16SekpBMSmS6kgzeaDS35ts9nyHmV5WgsqSxXD8PuEjx8HnZ3f0A_JtSHkknWHZ5Rr_4OF7jRtqra1bwt8kJypXtmhJIl.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/58e82c7f-3bea-40a7-98dd-5ebfcb6e6ea9.png)


1. 入力されたクエリをLLMが分析、必要な検索クエリを複数検討する（query_analyzer）
1. クエリを実行して、ベクトルストアからドキュメントを取得する（search）
1. 取得したドキュメントと元の質問文をLLMが比較して、一定以上の有用性があるかを10段階評価で評価（information_evaluator）
    - 有用な情報が一定数以上集まっていない場合、現状得られている有用な情報と合わせて再度検索クエリを検討する。（query_analyzer）
1. 問題がない場合は情報とクエリを合わせてLLMが回答を考える（response_generator）

各ノードごとに解説します。

# クエリ分析（query_analyzer）
- 質問を分析し、どの設計書からどういう検索語句で検索すればよいかを検討
- なお、情報が足りずに再度くる場合、現在の情報（有用な情報）を同じく渡すことで別の回答を期待
- また、複数クエリが欲しいものの、システムメッセージだとうまく複数を返してくれなかったので、ユーザメッセージに追加している
- LLMにレスポンス形式を強制させることでJSONでの受けをしやすくしている

```Python
if has_gap: 
    # 情報ギャップがある場合は、質問を再分析する
    system_message = """
    あなたは情報補完の専門家です。ユーザーの質問と現在の情報を分析し、不足している情報を特定してください。
    以下の情報の組み合わせを抽出してください：
    1. 質問のカテゴリ：「batch_design」（バッチ設計に関する質問）または「screen_design」（画面設計に関する質問）
    2. 重要キーワード：質問から抽出された不足している情報を検索するために役立つ新規の質問文
    
    """
else:
    system_message = """
    あなたは質問分析の専門家です。ユーザーからの質問を分析し、以下の情報の組み合わせを抽出してください：
    1. 質問のカテゴリ：「batch_design」（バッチ設計に関する質問）または「screen_design」（画面設計に関する質問）
    2. 重要キーワード：質問から抽出された検索に役立つ新規の質問文
    
    """

analysis_prompt = [
    SystemMessage(content=system_message),
    HumanMessage(content=f"以下の質問を分析してください。質問の組み合わせを３つ以上回答してください。：\n{user_query}")
]

analysis_result = llm.with_structured_output(Questions).invoke(analysis_prompt)
```

# 検索（search）
- クエリ分析で取得したクエリを利用して検索、得られたものをlistに格納
- なお、先ほど取得したカテゴリ情報は利用していない。（実装漏れ）

```Python
results = []
questions = state["questions"]
for question in questions:
    question_category = question.question_category
    search_query = question.search_query
    
    # ベクトルストアを読み込む
    vector_store = load_vector_stores(embedding_model_name)
    
    # 検索を実行
    search_results = vector_store.similarity_search_with_score(search_query, k=5)
    
    for doc, score in search_results:
        # スコアが閾値を超える場合のみ含める
        # if score < 0.8:  # 数値が小さいほど類似度が高い場合
        results.append({
            "content": doc.page_content,
            "metadata": doc.metadata,
            "score": float(score)
        })

```

# 情報評価（information_evaluator）
- 得られた文書データを関連性・有用性の2軸でそれぞれ10段階評価
- 現状、関連性の値＝有用性の値なので、あまり意味ないため有用性だけを使用
- 有用性が8以上のデータのみを利用しており、有用な文書が6個以上ないと追加検索を必要としている

```Python
system_message = """
    あなたは情報評価の専門家です。ユーザーの質問と検索結果を分析し、関連性の高い情報を選択してください。
    各検索結果に対して、以下の評価を行ってください：
    0. name: 文書1～文書XX
    1. relations: 関連度（0～10）：質問に対する情報の関連性
    2. usefulness: 有用性（0～10）：質問に答えるためにどれだけ役立つか
    3. appendix: 補足として、資料に対するコメントを追加してください。
    
    JSON形式で回答してください。
    """
    
# 検索結果を文字列として連結
docs_text = "\n\n".join([f"文書{i+1}:\n{doc['content']}" for i, doc in enumerate(relevant_documents)])

evaluation_prompt = [
    SystemMessage(content=system_message),
    HumanMessage(content=f"以下のテキストを確認して評価してください。各文書ごとに対して評価してください。質問: {user_query}\n\n検索結果:\n{docs_text}")
]

evaluation_result = llm.with_structured_output(Evaluates).invoke(evaluation_prompt)

has_gap = False
evaluates = evaluation_result.evaluates

for evaluate, relevant_document in zip(evaluates, relevant_documents):
    usefulness = evaluate.usefulness
    relation = evaluate.relation
    appendix = evaluate.appendix

    if usefulness > 7:
        useful_documents.append(relevant_document)

if len(useful_documents) < 6:
    has_gap = True
```

# 回答生成（response_generator）
- いままでに得られた文書情報をもとに回答を生成

```Python
system_message = """
    あなたは回答生成の専門家です。ユーザーの質問と収集された情報を分析し、正確で包括的な回答を生成してください。
    回答には以下の要素を含めてください：
    1. 質問への直接的な回答
    2. 関連する重要情報の要約
    3. 情報の出典（可能な場合）
    
    回答は日本語で、わかりやすく構造化してください。
    """

response_prompt = [
    SystemMessage(content=system_message),
    HumanMessage(content=f"質問: {user_query}\n\n利用可能な情報:\n{docs_text}")
]

response_result = llm.invoke(response_prompt)
```

# まとめ
とりあえず動くものができたので、いったん整理しました。
なお、ループが最大走ると、以下のように1分を超えることもあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/efa7815f-9231-4c23-9d88-72ef1c7e28b1.png)

本当はLLMに計画させて、どういう作業をすればよいかまでLLMに考えてもらいたかったところ。
今後の課題に。

なお、今回作成したものは以下にソースコードを置いています。

https://github.com/s-ono-123456/sampleagent