面對一個廣大又不熟悉的 code base,手上有一堆 bug 要修卻不知如何下手,只能乾瞪眼死盯著螢幕,相信這是每一位軟體工程師都有過的痛苦經驗。

好在這個時代我們有 AI,這邊就來教大家,如何白嫖免費使用 Google Gemini 1.5 Pro 為自己打造一個 Chat with Codebase 程式碼問答機器人!

對於只想直接在 local 使用的人,可以直接跳轉到「什麼是 Code Maestro?如何使用? 」章節閱讀。如果想知道實作原理,我們就繼續往下看囉!

實作 Chat with Codebase 程式碼問答 AI 機器人

我們使用免費的 Google Colab 來實作。

點擊我取得原始碼,可以透過 Open in Colab 按鈕,直接 clone 一份到自己的 colab 使用!

點擊上方按鈕 Open in Colab

如何免費用 Google Gemini 1.5 Pro 做 Chat with Codebase 程式碼問答 AI 機器人?

就能立刻 clone 原始碼並跳轉到 Google Colab 頁面

如何免費用 Google Gemini 1.5 Pro 做 Chat with Codebase 程式碼問答 AI 機器人?

以下我們依照每個區塊依序解釋。

解釋「 Init SSH key and GenAI 」區塊

這個區塊是在做基本設定

  • 需要把可以 clone repo 的 ssh private key 貼到對應字串變數中,執行時會將其暫時寫入到 colab container 中的 /.ssh/id_rsa ,後續執行 git clone 時才能正確取用到。
  • 將申請到的 Google Gemini api key 貼到 genai.configure(api_key="your_api_key") 之中,設定 Google GenAI lib
    • key 怎麼申請?問一下 google 大神!只要有 google 帳號都可以申請!
# @markdown # Init SSH key and GenAI
# @markdown 於程式碼中填入你的 ssh private key, gemini api key

pub_key = "your pub key (optional)"
# Add Private Key
private_key = """-----BEGIN OPENSSH PRIVATE KEY-----
your private key
-----END OPENSSH PRIVATE KEY-----
"""

import os

with open(os.path.join(".", "id_rsa"), "w") as f:
    f.write(private_key)

!rm -Rf ~/.ssh
!mkdir ~/.ssh

!mv /content/id_rsa ~/.ssh/id_rsa
!chmod 600 ~/.ssh/id_rsa

!echo -e "Host gitlab.com\\n\\tStrictHostKeyChecking no\\nHost github.com\\n\\tStrictHostKeyChecking no\\n" > ~/.ssh/config
!chmod 600 ~/.ssh/config

import google.generativeai as genai
# Add Gemini Api Key
genai.configure(api_key="your_api_key")

解釋 「Clone Repo」 區塊

這個區塊主要是

  • 讓使用者可以給定想要詢問的 repo SSH 位置、想要問的 branch。
    • default 表示 repo 的預設 branch
  • 設定後執行 git 指令,將對應 repo clone 下來,包含所有 submodule

補充說明,程式碼中的 # @param 是 Google Colab 的特殊註解,可以建立 Colab 輸入框 UI 方便使用,避免每次都要改 code。

# @markdown # Clone Repo
# @markdown #### Input Repo
repo_url = "[email protected]:Jim-Chang/KodingWork.git" # @param ["[email protected]:Jim-Chang/KodingWork.git"] {allow-input: true}
# @markdown #### Select Branch (選擇 default 時,使用 repo 的 default branch 設定)
branch = "default" # @param ["default", "master", "main", "stage", "develop"] {allow-input: true}

target_directory = "code_base"
git_clone_command = f"git clone {repo_url} --depth=1 {target_directory}"

!rm -Rf {target_directory}
!{git_clone_command}

if branch != "default":
  !cd {target_directory}; git checkout {branch}

!cd {target_directory}; git submodule init; git submodule update; git pull

解釋「合併 code base 並上傳 GenAI 」區塊

這邊就是整個問答機器人的實作核心之一。

原理如下:

  • 透過使用者給定的副檔名,我們將 repo 中符合此條件的檔案內容,全部合併到一份文字檔中,透過 Gemini File Api 將其上傳上去
  • 後續做 prompt 的時候,可以將上傳的檔案內容作為 prompt 的一部分,成為 context 讓 Gemini 讀取,從中尋找任務所需的答案。
# @markdown # 合併 code base 並上傳 GenAI

# @markdown ### 勾選要合併的副檔名
js = True # @param {type:"boolean"}
ts = False # @param {type:"boolean"}
tsx = False # @param {type:"boolean"}
json = False # @param {type:"boolean"}
py = False # @param {type:"boolean"}

file_extensions = []
if js:
    file_extensions.append("js")
if ts:
    file_extensions.append("ts")
if tsx:
    file_extensions.append("tsx")
if json:
    file_extensions.append("json")
if py:
    file_extensions.append("py")

all_file_contents = ""

for root, dirs, files in os.walk(target_directory):
    for file in files:
        if any(file.endswith(ext) for ext in file_extensions):
            file_path = os.path.join(root, file)
            relative_file_path = os.path.relpath(file_path, target_directory)

            with open(file_path, 'r') as f:
                file_content = f.read()

            all_file_contents += f"# Source File Path: {relative_file_path}\\n"
            all_file_contents += "```\\n"
            all_file_contents += file_content
            all_file_contents += "\\n```\\n"

with open("all_file_contents.txt", "w") as f:
    f.write(all_file_contents)

all_file_contents_prompt = genai.upload_file("all_file_contents.txt")
     

解釋「設定 Model」區塊

此區塊對 model 做一些基本參數的設定,包含:

  • model_name:目前可以選用 gemini 1.5 pro 或 gemini 1.5 flash
    • 兩者差異在於,pro 比較聰明,但速度慢,免費扣打只有 2 RPM,相對於 flash 就是稍微笨些,但速度快,免費扣打有 15 RPM,詳細限制可看此
    • 個人覺得使用上還是 pro 1.5 比較好用,如果短時間問太多被禁,就站起來休息一下,喝個水就能問了XD
  • temperature:設定模型的溫度,可以想像成自由度。雖越低會越照 prompt 裡的內容執行,但有時會因為過低,導致模型變笨,回答的結果不合預期。因此參考文獻中的建議,我們可以設定在 0.3 ~ 0.5 之間
  • top_p, top_k:這是透過另外兩個面向下去調整模型的自由度
    • OpenAI 文件中有提及,這兩個參數一般可以不用設定。透過 temperature 這個參數來控制會比較單純且容易,不然同時太多參數在控制自由度,可能會不如預期。
    • 因此這邊我們就參考他的建議,保留該參數的最大值(等同沒有做限制)
    • 這邊不深入解釋原理,因為超過本文範圍
  • safety_setting:主要是避免讓模型產生「不好」的結果
    • 但發現這部分有時會誤擋,蠻雷的
    • 反正都是給自己用,所以全部設定為 BLOCK_NONE
  • 最後將這些設定,全部丟入產生 model object
# @markdown # 設定 Model

from pathlib import Path
import hashlib

model_name = 'gemini-1.5-pro-latest' # @param ["gemini-1.5-pro-latest", "gemini-1.5-flash-latest"]
temperature = 0.3 # @param {type:"slider", min:0, max:1, step:0.1}
top_p = 1 # @param {type:"slider", min:0, max:1, step:0.1}
top_k = 32 # @param {type:"slider", min:0, max:100, step:1}

# Set up the model
generation_config = {
  "temperature": temperature,
  "top_p": top_p,
  "top_k": top_k,
  "max_output_tokens": 8192,
}

safety_settings = [
  {
    "category": "HARM_CATEGORY_HARASSMENT",
    "threshold": "BLOCK_NONE"
  },
  {
    "category": "HARM_CATEGORY_HATE_SPEECH",
    "threshold": "BLOCK_NONE"
  },
  {
    "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
    "threshold": "BLOCK_NONE"
  },
  {
    "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
    "threshold": "BLOCK_NONE"
  },
]

model = genai.GenerativeModel(model_name=model_name,
                              generation_config=generation_config,
                              safety_settings=safety_settings)

解釋「初始化/清除 對話紀錄 」 區塊

這區塊很簡單,就是初始化一個陣列,作為紀錄整個對話的歷史。

獨立成一個區塊的原因是,有時候問答太多次,AI 的回答可能會偏掉,此時我們只要執行一下這個區塊,就能清掉對話紀錄,重新開始。

# @markdown # 初始化/清除 對話紀錄
message_history = []

解釋「輸入 system prompt」區塊

這邊是在設定 system message 和 code prompt。

  • system_message:各位可以依照自己需求自行調整
  • code_prompt:
    • 讓模型知道如何從一整份文字檔中區分出「本來原始檔」所在檔案路徑,以及回覆的方式
    • 附上剛剛使用 Gemini File api 上傳後回傳的 object,即可成為 prompt 的一部分
  • 什麼?prompt 用中文也可以?
    • 可以啊XD只是 token 比較長,比較貴
    • 可能有時候控制不如預期(看模型特性)
    • 但這是 prototype,求快速實作,所以用中文比較方便囉!
# @markdown # 輸入 system prompt
system_message = "你是一個資深的軟體工程師,精通後端 python,依照提供的程式碼,解答使用者的問題" # @param ["你是一個資深的軟體工程師,精通後端 python,依照提供的程式碼,解答使用者的問題", "你是一個資深的軟體工程師,精通前端 react framework", "你是一個資深的軟體工程師,精通後端 nest.js,依照提供的程式碼,解答使用者的問題", "你是一個資深的軟體工程師,精通後端 node.js,依照提供的程式碼,解答使用者的問題"] {allow-input: true}

code_prompts = [
    "程式碼是由多個檔案組成,每份檔案的內容由 code block 包夾,每個 code block 起始處前一行會提供該檔案的路徑與檔名,回覆時請務必依照檔案對應的檔名提供給使用者",
    "## Source Code Begin ##",
    all_file_contents_prompt,
    "## Source Code End ##",
]

解釋「對話詢問」區塊

這也是整個機器人的另一個重要核心。

因為 Colab 沒辦法在他的基本 UI 下建立一個對話框,所以用 print 字串的方式來模擬。

  • 首先提供一個 user_message input,讓使用者可以作為對話輸入框使用
  • 輸入完畢,點擊左邊播放按鈕,會先把過往的對話紀錄從 message_history 撈出來,印出在畫面上,模擬對話框的歷史訊息顯示
  • 緊接的就是呼叫模型的 generate_content 方法,讓他產生答案
  • 產生的方式分為 streamingnon-streaming ,差異在於,當回應很長時,streaming 會和我們使用 ChatGPT 一樣逐漸地跑出文字來。反之則是等所有文字都產生完畢才會顯示。
  • 最後再將 AI 產生的文字加入 message_history 作為對話歷史
# @markdown # 對話詢問
from IPython.display import display, Markdown, clear_output

user_message = "" # @param {type:"string"}

def print_separate_line():
  print("====================================================================================")

print("## Message History ##")
print_separate_line()
for message in message_history:
  if message.startswith("ai:"):
    display(Markdown(message))
  else:
    print(message)
  print_separate_line()

if user_message:
  print(f"user:\\n{user_message}")
  print_separate_line()

print("## AI Generating...")
print_separate_line()

prompt_parts = [
    system_message,
    *code_prompts,
    *message_history,
]

if user_message:
  prompt_parts.append(f"user:\\n{user_message}")

streaming = True # @param {type:"boolean"}
display_handle = display(Markdown(""), display_id=True)
ai_result = ""

if streaming:
  response = model.generate_content(prompt_parts, stream=True, request_options={'timeout': 600})
  for chunk in response:
    ai_result += chunk.text
    display_handle.update(Markdown(ai_result))

else:
  response = model.generate_content(prompt_parts)
  ai_result = response.text
  display(Markdown(ai_result))

if user_message:
  message_history.append(f"user:\\n{user_message}")

message_history.append(f"ai:\\n{ai_result}")

整個 Chat with Codebase 程式碼問答 AI 機器人的實作就是這樣啦!其實概念蠻簡單的,就是類似 RAG 的概念,把你想問的程式碼給他作為參考來回覆!

什麼是 Code Maestro?如何使用?

相對於使用 Google Colab 建立的機器人,雖然方便、不用管理環境,但是卻有 UI 較不友善、且免費版隨時會被斷線的困擾。

因此我利用 gradio 這個現代的 AI prototype framework 快速建了一個前端出來,內部邏輯概念與 Google Colab 的版本極為相似,因此這邊就不再贅述原理,直接進如何使用教學。

首先確認電腦環境中有 python 3.11.6,建議使用 pyenv 安裝,當然其他用習慣的方式也行

pyenv install 3.11.6

clone Code Maestro repo 下來

git clone [email protected]:Jim-Chang/code-maestro.git

cd 進去 repo 中,設定 pyenv 使用 3.11.6,並透過 poetry 安裝依賴套件

cd code-maestro
pyenv local 3.11.6
poetry env use 3.11.6
poetry install

安裝完畢,可以直接啟動 server

./start.sh

打開瀏覽器,前往 http://127.0.0.1:7860 ,會看到以下畫面,只有一個對話過程、輸入匡和兩個按鈕

如何免費用 Google Gemini 1.5 Pro 做 Chat with Codebase 程式碼問答 AI 機器人?

正式開始使用前,先在畫面最上方切換到 Settings Tab 做設定。

首先對 Model Setting 做設定。

一樣填入你的 api key,選擇想要用的 model (pro、flash),溫度和系統訊息可以用預設,或依照自己喜好修改。

修改完畢按下 save settings 儲存。

如何免費用 Google Gemini 1.5 Pro 做 Chat with Codebase 程式碼問答 AI 機器人?

接下來是最重要的部分,設定你要詢問的 repo。

輸入 repo 的 SSH 位置、想要切換到的 branch,default 表示 repo 的預設 branch,branch name 可以透過輸入或是下拉選單選擇。

接著勾選你想要合併的檔案副檔名,只有符合此副檔名的檔案內容,才會合併並上傳到 Gemini。

設定好後,按下 Use Repository For Chat ,他會自動幫你處理所有 clone、merge、upload 等任務,需要一小段時間。

(備註:做 clone 時,會使用 host 預設的 git 設定取用 ssh key,只要日常開發時可以 clone 的 repo,基本上都能正常使用)

如何免費用 Google Gemini 1.5 Pro 做 Chat with Codebase 程式碼問答 AI 機器人?

到這裡就可以切換到 Chat Tab,開始 Chat with Codebase 囉!

如何免費用 Google Gemini 1.5 Pro 做 Chat with Codebase 程式碼問答 AI 機器人?

如何使用 Code Maestro 的 Chat with Code Diff?

此功能只有在 Code Maestro 中有實作。

首先一樣切換到 Setting Tab,往下到 Diff Setting 區塊。

勾選 Enable Diff ,然後在第一個 Select a Branch For Diff 設定基底 branch name,第二個設定你想要比較的 branch name。

這邊建議使用時,上面 Repository Setting 的 branch 要設定為這邊的兩者其一會比較好,並且要先 clone 過才行設定 Diff Setting。

如何免費用 Google Gemini 1.5 Pro 做 Chat with Codebase 程式碼問答 AI 機器人?

然後就可以回去 Chat Tab 來詢問了。我最常用的時機就是在開 PR 的時候寫描述使用,可以參考以下 prompt,他就能幫忙寫出一個輪廓,簡單複製貼上修改一下就可以囉!

我現在要將這次的修改開 PR 給同事 Review,我需要讓同事知道這次改動的細節,並以列點、重點摘要說明讓他能夠快速理解改動內容,並開始 Review。
我需要你幫忙依照以上需求,整理出 PR 的描述。
如何免費用 Google Gemini 1.5 Pro 做 Chat with Codebase 程式碼問答 AI 機器人?

或是有時候要查看同事的 PR,也可以用這個方法叫 AI 幫忙整理出修改的內容,參考以下 prompt:

請你幫我做 code review,幫我完成以下任務:

- 詳細解釋修改了些什麼程式與邏輯
- 確認此次修改是否產生任何 bugs
- 確認此次修改是否會造成任何 side effect
- 確認此次修改是否有語意模糊、或架構不清楚的地方,提出來並給予建議

這樣在看 PR 的時候就可以有更多的資訊輔助並加快速度啦!

(如果有一天他能直接幫我看 PR 該有多好XD)

這個功能的實作原理也很簡單,透過 git diff 產生出兩個 branch 的差異,並上傳到 Gemini 作為 prompt 參考資料的一部分,這樣他就能知道兩個 branch 的改動內容囉!

AI 能做的事情遠超過以上 demo,你也可以叫他產生 api 文件,幫忙實作 api…等等各種腦洞。就靠各位自行創意和嘗試囉!

有任何建議或修改也歡迎大家開 PR 給我!希望大家都能藉著 AI 的肩膀跑得更快XD

Written by J
雖然大學唸的是生物,但持著興趣與熱情自學,畢業後轉戰硬體工程師,與宅宅工程師們一起過著沒日沒夜的生活,做著台灣最薄的 intel 筆電,要與 macbook air 比拼。 離開後,憑著一股傻勁與朋友創業,再度轉戰軟體工程師,一手扛起前後端、雙平台 app 開發,過程中雖跌跌撞撞,卻也累計不少經驗。 可惜不是那 1% 的成功人士,於是加入其他成功人士的新創公司,專職開發後端。沒想到卻在採前人坑的過程中,拓寬了眼界,得到了深層的領悟。