iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0
生成式 AI

一起來打造 PTT 文章智慧問答系統!系列 第 19

【Day 19】PTT 文章智慧問答系統 - 整合 Gemini LLM 回答語意查詢問題

  • 分享至 

  • xImage
  •  

Hi大家好,
這是我參加 iT 邦幫忙鐵人賽的第 1 次挑戰,這次的主題聚焦在結合 Python 爬蟲、RAG(檢索增強生成)與 AI,打造一套 PTT 文章智慧問答系統。在過程中,我會依照每天進度上傳程式碼到 GitHub ,方便大家參考學習。也歡迎留言或來信討論,我的信箱是 gerryearth@gmail.com


今天我們將實作 Retrieval-Augmented Generation (RAG) 的最後一步: 利用 Gemini 大語言模型回應使用者問題!


今日目標

  • 了解 RAG 架構的生成步驟
  • 使用 Gemini API 產生語意問答結果
  • 實作完整語意查詢 + 回答的 Django API

Gemini 回答 API 實作

我們將實作一個簡單的 API:

  • 輸入:使用者查詢文字
  • 流程:向量查詢 → 撈出段落 → 拿到整篇文章 → 丟給 Gemini → 回傳回答
  • 輸出:回答內容

以下介紹幾個重點步驟:

Step 1:定義 Prompt 模板

prompt_template = """
你是一個根據文章回答問題的助理。以下是PTT相關文章內容:
---
{merge_text}
---
根據以上內容,請用純文字回答這個問題:
{question}
"""

Step 2:組合段落的文章作為 merge_text

merge_text = "\n".join([f"Title:{a.title} - Content:{a.content}" for a in related_articles])

Step 3:發送 Gemini API 並獲取回答

我們使用 LangChain 搭配 Gemini 的 GPT-4o 模型來建立一個「問答鏈」(Question Answering Chain),並用來針對指定文本回答問題:

model = ChatGoogleGenerativeAI(
                model="gemini-2.0-flash",
                temperature=0,
                google_api_key=env_settings.google_api_key
            )
ptt_template = PromptTemplate(
                input_variables=["merge_text", "question"],
                template="""
                根據以下PTT的文章內容以純文字回答問題:
                ---
                {merge_text}
                ---
                問題:{question}
                """
            )
chain = ptt_template | model
answer = chain.invoke({"merge_text": merge_text, "question": question}).content

Step 4:整合到 Django API

最後我們整理並組合成完整的 API 在 article/views.py

class SearchAPIView(APIView):
    @extend_schema(
        methods=("POST",),
        description="輸入question(問題)與top_k(想查詢的文章片段數),藉由LLM與向量資料庫得到question、answer(相關回答)、related_articles(相關文章)。",
        request=QueryRequestSerializer,
        responses=QueryRequestSerializer
    )
    def post(self, request):
        import asyncio
        query_request_serializer = QueryRequestSerializer(data=request.data)
        if not query_request_serializer.is_valid():
            Log.objects.create(level='ERROR', category='user-search', message='查詢參數不合法', )
            return Response(query_request_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        question = query_request_serializer.validated_data.get("question")
        top_k = query_request_serializer.validated_data.get("top_k")
        # 查詢Pinecone embeddings內容
        try:
            try:
                asyncio.get_running_loop()
            except RuntimeError:
                asyncio.set_event_loop(asyncio.new_event_loop())
            vector_store = PineconeVectorStore(
                index=Pinecone(
                    api_key=env_settings.PINECONE_API_KEY
                ).Index(env_settings.PINECONE_INDEX_NAME),
                embedding=GoogleGenerativeAIEmbeddings(model=env_settings.GOOGLE_EMBEDDINGS_MODEL, google_api_key=SecretStr(env_settings.GOOGLE_API_KEY))
            )
            top_k_results = vector_store.similarity_search_with_score(question, k=top_k, )
        except Exception as e:
            Log.objects.create(level='ERROR', category='user-search', message=f'查詢Pinecone embeddings內容發生錯誤: {e}',
                               traceback=traceback.format_exc())
            return Response({"error": f"查詢Pinecone embeddings內容發生錯誤: {str(e)}"},
                            status=status.HTTP_500_INTERNAL_SERVER_ERROR)
        # 從資料庫找出文章內容並合併
        try:
            match_ids = [match[0].metadata['article_id'] for match in top_k_results]
            query_request_serializer.related_articles = Article.objects.filter(id__in=match_ids)
            merge_text = "\n".join(
                [f"Title:{a.title} - Content:{a.content}" for a in query_request_serializer.related_articles])
            if len(merge_text) > 128000:
                Log.objects.create(level='ERROR', category='user-search', message='回傳文章總字數過長,請嘗試減少top_k')
                return Response(
                    {"error": "回傳文章總字數過長,請嘗試減少top_k"},
                    status=status.HTTP_400_BAD_REQUEST)
        except (KeyError, TypeError) as e:
            Log.objects.create(level='ERROR', category='user-search', message=f'從資料庫找出文章內容發生錯誤: {e}',
                               traceback=traceback.format_exc())
            return Response(
                {"error": f"從資料庫找出文章內容發生錯誤: {str(e)}"},
                status=status.HTTP_500_INTERNAL_SERVER_ERROR)
        # 請求 ChatGPT 回答問題
        try:
            model = ChatGoogleGenerativeAI(
                model="gemini-2.0-flash",
                temperature=0,
                google_api_key=env_settings.GOOGLE_API_KEY,
            )
            ptt_template = PromptTemplate(
                input_variables=["merge_text", "question"],
                template="""
                根據以下PTT的文章內容以純文字回答問題:
                ---
                {merge_text}
                ---
                問題:{question}
                """
            )
            chain = ptt_template | model
            answer = chain.invoke({"merge_text": merge_text, "question": question}).content
        except Exception as e:
            Log.objects.create(level='ERROR', category='user-search', message=f'請求ChatGPT回答發生錯誤: {e}',
                               traceback=traceback.format_exc())
            return Response({"error": f"請求ChatGPT回答問題發生錯誤: {str(e)}"},
                            status=status.HTTP_500_INTERNAL_SERVER_ERROR)
        try:
            serializer = QueryRequestSerializer(instance={
                "question": question,
                "answer": answer,
                "related_articles": Article.objects.filter(id__in=match_ids),
            })
            return Response(serializer.data, status=status.HTTP_200_OK)
        except Exception as e:
            Log.objects.create(level='ERROR', category='user-search', message=f'序列化輸出資料失敗: {e}',
                               traceback=traceback.format_exc())
            return Response({'error': '序列化輸出資料失敗'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

相應套件:

from langchain_google_genai import ChatGoogleGenerativeAI,GoogleGenerativeAIEmbeddings
from env_settings import EnvSettings
from langchain_pinecone import PineconeVectorStore
from pinecone import Pinecone
from langchain_core.prompts import PromptTemplate
from pydantic import SecretStr
from .serializers import QueryRequestSerializer

env_settings = EnvSettings()

Step 5:建立 Serializer

article/serializers.py 新增:

class QueryRequestSerializer(serializers.Serializer):
    question = serializers.CharField(help_text="查詢內容", required=True, max_length=100, min_length=1)
    top_k = serializers.IntegerField(help_text="控制段落的查詢數量 (預設 3)", default=3, write_only=True, min_value=1, max_value=10)

    answer = serializers.CharField(required=False, read_only=True)
    related_articles = ArticleSerializer(many=True, read_only=True)

Step 6:新增 API 端點

urls.py 新增 search API 端點:

urlpatterns = [
    ...
    path('search/', views.SearchAPIView.as_view(), name='search'),
]

最後呈現

https://ithelp.ithome.com.tw/upload/images/20250819/201728346Zo2vzVCgq.png


實際範例

使用者輸入問題:

輝達股票

Gemini 回覆:

根據文章內容,關於輝達股票:\n\n*   川普持有輝達股票,截至去年底持有61.5萬至130萬美元的輝達股票。\n*   川普允許輝達恢復對中國銷售人工智慧(AI)晶片,條件是美國政府可抽取15%營收分成。\n*   輝達因政策轉向預計可獲數十億美元收益。\n*   7月15日商務部宣布恢復對華銷售當天,輝達股價首次突破170美元,市值超過4兆美元。\n*   川普近期多次稱讚輝達執行長黃仁勳。\n*   過去兩年他似乎加碼輝達部位。

結果如下:
https://ithelp.ithome.com.tw/upload/images/20250819/20172834uJ5iS0Xyu2.png


在明天**【Day 20】封裝語意查詢流程 - 讓程式碼更乾淨且可維護**,我們會把語意查詢與生成回答的流程 封裝成一個服務,讓 API 只需要單純呼叫這個函式就能取得結果。這樣可以讓程式碼更好維護。


上一篇
【Day18】Pinecone 整合實戰 - 從資料向量化到自動任務串接
下一篇
【Day 20】封裝語意查詢流程 - 讓程式碼更乾淨且可維護
系列文
一起來打造 PTT 文章智慧問答系統!20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言