system prompt 對於一個 Agent 來說非常重要,好的提示詞(prompt)可以好好的約束模型的作為,今天這篇要做的事就是定好它,讓模型可以有明確的「行為規範」、清楚的「工作環境認知」以及要把上一篇轉為 XML 的工具也一併給到模型手上。
我們定義好一個固定的格式,未來需要更改時只需要透過更改傳入此函數的參數就可以方便地做更動:
# src/meowgent/prompt/system_prompt.py
from typing import Optional
import textwrap
import platform
from .mcp_schema import get_all_xml
def get_system_prompt(model_name: str, path: str, rule: Optional[str] = None, enable_tools: bool = True) -> str:
enable_tools的作用後面會解釋。
我們可以添加角色的說明,讓模型更理解應該如何去回答使用者。
這點大家應該都比較明白,就像是你日常生活中跟不同人講話的口氣都不盡相同,跟公司領導講話要客氣、而跟朋友聊天就可以比較隨便。
而最主要的部分是讓模型不會胡作非為,明明給定了工作目錄,卻跑到外部去亂改亂看。
但這規範其實屬於防君子不防小人。
因為我們只跟模型說不能這樣做,但沒有訂明確的邏輯來攔截那些偷跑出工作目錄的調用。
這裡,我們做一個函數來攔截這樣的現象:
目錄處理的部分有一個之前沒用過的函數resolve(),它是用來把相對路徑完整展開為絕對路徑的(如展開.、..,而expanduser()只負責展開~)。
路徑處理完後用is_relative_to()判斷path是否在cwd底下,回傳布林值。# src/meowgent/tool.py from ... from pathlib import Path ... def _path_check(path: str) -> bool: cwd = Path.cwd().resolve() # 當前工作目錄 path = Path(path).expanduser().resolve() return path.is_relative_to(cwd)函數處理完還需要在下方的工具函數(
read_file()、write_file()、edit_file()、grep_search())的開頭上加入以下條件判斷:# src/meowgent/tool.py def ...(...) -> ...: if not _path_check(file_path): return "路徑位於工作目錄之外,請更改路徑或向使用者提出更換工作目錄" ...
最後我們還可以加上不要過度調用工具的提示。
在我還沒加上這條規定之前,常常發生我跟模型說嗨,他卻調用工具去查看一堆文檔,最後才跟我打招呼,非常煩人!
這裡挖個坑(會不會填上就不一定啦,哈哈),不知道最近大家有沒有聽說一個新形式的模型叫做 Jev,它來自 TypeSafe AI。
傳統的 LLM 走的都是「生成式」路線,而它走的是「決策式」,當我們需要一些「確定性決策」就可以使用它。
過去要做到這樣的方式往往只能透過 prompt 約束結構性的回傳(如 JSON Schema)來逼迫模型,成本高且延遲長,
而 Jev 不生成文字,取而代之的是直接一次產生固定的結構來表示決策(它可以透過支援的三種判斷形式來做快速的決策,包含**「是非」、「選擇」以及「信心分數」),免除了傳統 LLM 每吐出一個 token 都需要執行一次前向傳播及 KV-Cache 讀取,運算次數得到數量級的減少**。
說這麼多,其實可以簡單把它理解為是一個「智能的 if 判斷」!這邊可以用 Jev 來判斷在不需要工具時,把工具給抽走,未來有機會再一併實作給各位看。
看到下方代碼的文字部分,使用 textwrap.dedent() 來讓我們可以把程式碼做縮排美觀,而最後還要 .strip() 把頭尾的換行清掉。
rule = textwrap.dedent("""跟""").strip()都會分別造成一行的換行。
# src/meowgent/prompt/system_prompt.py
...
def get_system_prompt(...) -> ...:
if rule is None: # 預設角色規範
rule = textwrap.dedent("""
你是一名專業的程式碼編程助理。
1. 協助使用者解決專案開發、檔案編輯與命令列操作。
2. 調用工具進行任何動作時,僅限在給定的工作目錄進行。
3. 必要時才調用工具,不過度使用工具,
只有當任務明確需要檔案或指令操作時,才調用對應工具。
""").strip()
如果
rule不為空,也就是裡面已經有內容,就不做動作,保持傳入的規範。
接著來完成工作環境的部分:
_path_check() 做攔截。# src/meowgent/prompt/system_prompt.py
def get_system_prompt(...) -> ...:
...
environment = textwrap.dedent(f"""
- 作業系統:{platform.system()}
- 工作目錄:{path}
- 目前模型:{model_name}
""").strip() # 環境資訊
最後一個是 MCP 調用說明:
還記得在 agent.py 有做最大工具使用次數的限制嗎?
達到限制後,這裡直接把工具拔掉,不讓它用,也就是判斷 enable_tools 為 True 才有工具,否則 tool_section 會是空字串。
# src/meowgent/prompt/system_prompt.py
def get_system_prompt(...) -> ...:
...
tool_section = ""
if enable_tools:
mcp_tool = get_all_xml() # 可用工具
tool_rule = textwrap.dedent(f"""
當你需要使用工具時,必須嚴格使用 <tool_call> 標籤包裹 JSON,格式如下:
<tool_call>
{{"name": "工具名稱", "arguments": {{"參數名稱": "值"}}}}
</tool_call>
若需要同時執行多個無相依性的操作(例如同時讀取多個檔案或多個網頁),
你可以在同一次回答中輸出多個 <tool_call>...</tool_call> 區塊,
系統將會並發平行執行它們以提升效率。
""").strip() # 工具調用格式
tool_section = f"\n# 可用工具\n{mcp_tool}\n\n# 工具調用格式規範\n{tool_rule}"
三個區塊已經製作好了,再來將它們給拼起來:tool_section 的部分,判斷裡面有無內容,明確告訴模型。
# src/meowgent/prompt/system_prompt.py
def get_system_prompt(...) -> ...:
...
tool_section = tool_section if tool_section else "無可用工具"
return textwrap.dedent(f"""
# 角色與行為準則
{rule}
# 當前環境資訊
{environment}
# 工具調用
{tool_section}
""").strip()
目前 system prompt 的傳輸流程為下圖:
我們剛已經完成了第一點,接下來我們把它封裝進多輪紀錄裡:
原先我們的 temp_history_messages 是複製了一份 self.history_messages 做的,現在改為直接建立一份串列,把 system prompt 當成串列第一個物件,後方再加上多輪紀錄。
如此一來,system prompt 永遠保持在索引 0 的位置。
# src/meowgent/agent.py(原先)
...
class Agent():
...
def chat(...):
...
temp_history_messages = list(self.history_messages)
if is_last_turn:
temp_history_messages.append(...)
...
...
# src/meowgent/agent.py
class Agent():
...
def chat(...):
...
is_last_turn = ...
while turns < self.max_turns:
temp_history_messages = [
{
"role": "system",
"content": get_system_prompt(
model_name=self.model_name,
path=self.path,
rule=self.rule,
enable_tools = not is_last_turn # 最後一輪禁用工具說明
)
}
] + self.history_messages
if is_last_turn:
...
至於為什麼要在每次呼叫模型時都重新獲取一次 system prompt 呢?
- 未來會做模型切換、工作目錄切換等需要動態更新 system prompt 的功能。
- 當工具使用次數達到限制後也需要動態的將工具給移除。
再來,第三點,由於先前傳入的就是 temp_history_messages 所以不需做修改,這麼一來,我們就成功的把 system prompt 交到模型手中了!
至今為止的 9 篇,我們完成了不少組件,
但你有沒有發現,原先被我們放在 agent.py 的 if __name__ == "__main__": 被刪掉了?
沒錯,現在的情況蠻尷尬的,我們的程式沒有入口了!
明天開始的五篇,我們開始來處理這個問題(也就是說我們的程式要在 Day 14 才能完整運行起來,蠻搞笑的哈哈)。