使用Python從零搭建一個能用的AI Agent
上個月接了個私活,甲方要求做一個「智能客服助手」,能查訂單、能查物流、還能根據(jù)用戶問題自動判斷該調(diào)哪個工具。說白了就是要一個 AI Agent。
我一開始想著,不就是大模型 + 函數(shù)調(diào)用嘛,兩天搞定。結(jié)果整整折騰了一周——工具調(diào)用的參數(shù)解析炸了、多輪對話的上下文丟了、Agent 陷入死循環(huán)瘋狂調(diào)同一個函數(shù)……
踩完這些坑之后,我把整個流程抽成了一套還算能復用的模板。今天把核心代碼和踩坑記錄都貼出來,希望能幫你少走點彎路。
先說結(jié)論
| 要點 | 說明 |
|---|---|
| 核心原理 | LLM 做決策大腦 + Tool Calling 做手腳 |
| 最小依賴 | openai SDK + 任何兼容 OpenAI 協(xié)議的 API |
| 關鍵難點 | 工具描述的 prompt 工程、多輪上下文管理、循環(huán)調(diào)用兜底 |
| 代碼量 | 核心 Agent 循環(huán)不到 100 行 |
| 適用模型 | GPT-4o、Claude 3.5、Gemini Pro 等支持 function calling 的模型 |
什么是 AI Agent?別被概念唬住
圈子里關于 Agent 的定義吵了一年了,各種框架花里胡哨。但對我這種干活的人來說,Agent 的本質(zhì)就一句話:
讓大模型自己決定「下一步做什么」,而不是你在代碼里用 if-else 替它決定。
傳統(tǒng)的 LLM 應用是這樣的:
用戶提問 → 你拼 prompt → 調(diào) LLM → 返回文本
Agent 的流程是這樣的:
用戶提問 → LLM 判斷要不要用工具 → 用哪個工具 → 執(zhí)行工具拿結(jié)果 → 把結(jié)果喂回 LLM → LLM 再判斷……直到它覺得可以回答了
核心就是一個 ReAct 循環(huán)(Reasoning + Acting),模型自己推理、自己行動、自己觀察結(jié)果、再推理。
好,概念到此為止,開始寫代碼。
環(huán)境準備
依賴極簡,就一個 openai 的 SDK:
pip install openai
因為我們用的是兼容 OpenAI 協(xié)議的接口,所以不管你背后調(diào)的是 GPT、Claude 還是 Gemini,代碼都一樣。我自己開發(fā)的時候需要頻繁切模型對比效果,折騰了一圈發(fā)現(xiàn)最省事的方案是用聚合 API,改個 base_url 就能切模型,不用管各家的鑒權(quán)差異。
from openai import OpenAI
client = OpenAI(
api_key="your-key",
base_url="https://api.ofox.ai/v1" # 聚合接口,一個 Key 用所有模型
)
第一步:定義工具(Tools)
Agent 的「手腳」就是工具。你得先告訴大模型有哪些工具可用、每個工具接收什么參數(shù)。
我以那個客服場景為例,定義兩個工具——查訂單和查物流:
# 模擬的業(yè)務函數(shù)
def query_order(order_id: str) -> dict:
"""根據(jù)訂單號查詢訂單信息"""
# 實際項目里這里查數(shù)據(jù)庫
fake_db = {
"ORD001": {"order_id": "ORD001", "product": "機械鍵盤", "status": "已發(fā)貨", "amount": 399},
"ORD002": {"order_id": "ORD002", "product": "顯示器支架", "status": "待付款", "amount": 89},
}
return fake_db.get(order_id, {"error": f"訂單 {order_id} 不存在"})
def query_logistics(order_id: str) -> dict:
"""根據(jù)訂單號查詢物流信息"""
fake_logistics = {
"ORD001": {"carrier": "順豐", "tracking_no": "SF1234567890", "status": "在途中,預計明天到"},
}
return fake_logistics.get(order_id, {"error": f"訂單 {order_id} 暫無物流信息"})
然后把工具描述成 OpenAI function calling 要求的格式:
tools = [
{
"type": "function",
"function": {
"name": "query_order",
"description": "根據(jù)訂單號查詢訂單詳情,包括商品名、狀態(tài)、金額",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "訂單編號,格式如 ORD001"
}
},
"required": ["order_id"]
}
}
},
{
"type": "function",
"function": {
"name": "query_logistics",
"description": "根據(jù)訂單號查詢物流狀態(tài),包括快遞公司、運單號、當前狀態(tài)",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "訂單編號,格式如 ORD001"
}
},
"required": ["order_id"]
}
}
}
]
這里有個坑我必須提一下:description 寫得好不好,直接決定模型會不會正確地選工具。我一開始 query_logistics 的描述寫的是「查詢物流」四個字,結(jié)果模型經(jīng)常把「我的訂單到哪了」這種問題路由到 query_order 上去。后來我把描述改詳細了,加上「快遞公司、運單號、當前狀態(tài)」這些關鍵詞,準確率一下就上來了。
第二步:搭建 Agent 主循環(huán)
這是整個 Agent 的核心,也就是 ReAct 循環(huán)。邏輯很直白:
- 把用戶消息發(fā)給 LLM
- 如果 LLM 返回了 tool_calls,就執(zhí)行對應的函數(shù)
- 把函數(shù)結(jié)果塞回消息列表,再發(fā)給 LLM
- 重復,直到 LLM 不再調(diào)用工具,直接返回文本
import json
# 工具名 → 實際函數(shù)的映射
TOOL_MAP = {
"query_order": query_order,
"query_logistics": query_logistics,
}
SYSTEM_PROMPT = """你是一個電商客服助手。你可以幫用戶查詢訂單信息和物流狀態(tài)。
請用簡潔友好的語氣回復。如果用戶沒有提供訂單號,請先詢問訂單號。"""
def run_agent(user_input: str, messages: list = None, max_turns: int = 5) -> str:
"""
運行 Agent 主循環(huán)
max_turns: 最大工具調(diào)用輪次,防止死循環(huán)
"""
if messages is None:
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
messages.append({"role": "user", "content": user_input})
for turn in range(max_turns):
response = client.chat.completions.create(
model="gpt-4o", # 換成 claude-3.5-sonnet 等也行
messages=messages,
tools=tools,
tool_choice="auto", # 讓模型自己決定要不要調(diào)工具
)
msg = response.choices[0].message
messages.append(msg) # 把 assistant 的回復加入上下文
# 如果沒有工具調(diào)用,說明模型準備好直接回答了
if not msg.tool_calls:
return msg.content
# 執(zhí)行每個工具調(diào)用
for tool_call in msg.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
print(f" [Agent] 調(diào)用工具: {func_name}({func_args})")
# 執(zhí)行函數(shù)
if func_name in TOOL_MAP:
result = TOOL_MAP[func_name](**func_args)
else:
result = {"error": f"未知工具: {func_name}"}
# 把工具執(zhí)行結(jié)果塞回消息列表
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result, ensure_ascii=False)
})
return "抱歉,我處理這個問題遇到了困難,請聯(lián)系人工客服。"
注意最后那個兜底的 return——這就是 max_turns 的作用。我之前沒加這個,測試的時候 Agent 對一個不存在的訂單號瘋狂調(diào) query_order,調(diào)了十幾次才超時報錯。加個上限,超過 5 輪強制退出,返回一個友好的兜底話術。
第三步:跑起來看看效果
if __name__ == "__main__":
# 測試 1:正常查詢
print("=" * 50)
print("用戶:幫我看看 ORD001 到哪了")
print("Agent:", run_agent("幫我看看 ORD001 到哪了"))
print()
# 測試 2:需要先查訂單再查物流
print("=" * 50)
print("用戶:ORD001 買的什么?快遞到哪了?")
print("Agent:", run_agent("ORD001 買的什么?快遞到哪了?"))
print()
# 測試 3:缺少訂單號
print("=" * 50)
print("用戶:我想查一下我的快遞")
print("Agent:", run_agent("我想查一下我的快遞"))
實際運行輸出大概長這樣:
==================================================
用戶:幫我看看 ORD001 到哪了
[Agent] 調(diào)用工具: query_logistics({"order_id": "ORD001"})
Agent:您的訂單 ORD001 由順豐快遞承運,運單號 SF1234567890,目前在途中,預計明天到達。
==================================================
用戶:ORD001 買的什么?快遞到哪了?
[Agent] 調(diào)用工具: query_order({"order_id": "ORD001"})
[Agent] 調(diào)用工具: query_logistics({"order_id": "ORD001"})
Agent:您的訂單 ORD001 購買的是機械鍵盤(399元),已發(fā)貨??爝f由順豐承運,運單號 SF1234567890,目前在途中,預計明天到。
==================================================
用戶:我想查一下我的快遞
Agent:好的,請?zhí)峁┮幌履挠唵尉幪?,我?guī)湍樵兾锪餍畔ⅰ?/p>
第二個測試案例是我覺得最能體現(xiàn) Agent 價值的——用戶一句話包含兩個意圖,模型自己判斷需要調(diào)兩個工具,并行調(diào)用(GPT-4o 支持一次返回多個 tool_calls),然后把兩個結(jié)果整合成一段話回復。這種邏輯你用 if-else 寫,嵌套能寫到懷疑人生。
踩坑記錄
坑 1:tool_call 的 arguments 不一定是合法 JSON
是的你沒看錯。模型偶爾會返回不合法的 JSON 字符串,尤其是一些小參數(shù)量的模型。我遇到過返回 {order_id: "ORD001"} 少引號的情況。
解決方案很暴力但有效:
try:
func_args = json.loads(tool_call.function.arguments)
except json.JSONDecodeError:
# 嘗試用 ast.literal_eval 兜底,再不行就報錯
import ast
try:
func_args = ast.literal_eval(tool_call.function.arguments)
except:
result = {"error": "參數(shù)解析失敗"}
# 繼續(xù)把 error 喂給模型,讓它重試
坑 2:多輪對話的 messages 會越來越長
每次工具調(diào)用的結(jié)果都要追加到 messages 列表里,聊幾輪之后 token 數(shù)蹭蹭漲。我那個客服場景用戶平均聊 8-10 輪,到后面經(jīng)常超 token 限制。
我的做法是加一個簡單的滑動窗口:
def trim_messages(messages: list, max_tokens: int = 8000) -> list:
"""保留 system prompt + 最近的消息"""
system_msg = messages[0] # system prompt 永遠保留
recent = messages[1:]
# 粗略估算:1個中文字符約2個token
while len(json.dumps(recent, ensure_ascii=False)) > max_tokens * 2 and len(recent) > 2:
recent.pop(0)
return [system_msg] + recent
粗暴但管用。正經(jīng)生產(chǎn)環(huán)境可以用 tiktoken 精確計算 token 數(shù)。
坑 3:工具描述要站在「模型的視角」寫
這個我前面提過了,但值得再強調(diào)一下。你覺得理所當然的信息,模型不一定知道。比如我有個工具叫 get_refund_policy,一開始描述是「獲取退款政策」。結(jié)果用戶問「買了 7 天了還能退嗎」,模型根本不會調(diào)這個工具——因為在它看來這是個關于時間的問題,不是關于「政策」的問題。
后來我改成:「獲取退款政策信息,當用戶詢問能否退款、退款條件、退款時限、退貨流程等問題時使用」,一下就準了。
寫工具描述的時候,想想用戶會怎么問,而不是這個函數(shù)在代碼里叫什么。
坑 4:模型幻覺——編造工具參數(shù)
用戶說「幫我查一下訂單」沒給訂單號,正常情況模型應該反問。但我遇到過 GPT-3.5 直接編一個訂單號 ORD12345 去調(diào)工具的情況。GPT-4o 和 Claude 3.5 好很多,基本不會出現(xiàn)。
如果你用的模型不夠強,可以在 system prompt 里加一句硬約束:
重要:如果用戶沒有提供必要的參數(shù)信息,你必須先向用戶詢問,絕對不能自行編造參數(shù)。
往更完整的方向擴展
上面這套代碼是一個最小可用的 Agent。實際項目你可能還需要:
- 記憶持久化:把 messages 存到 Redis/數(shù)據(jù)庫,支持用戶下次繼續(xù)聊
- 流式輸出:
stream=True,不然用戶等 Agent 調(diào)完工具再回復,體驗很差 - 工具權(quán)限控制:不同用戶能用不同的工具
- 可觀測性:記錄每次 LLM 調(diào)用的 token 數(shù)、延遲、工具調(diào)用鏈路,方便排查問題
這些我后續(xù)可能會單獨寫。今天這篇就聚焦在核心循環(huán)和踩坑上。
小結(jié)
AI Agent 聽起來高大上,但拆開了就三件事:定義工具、讓模型選工具、執(zhí)行工具把結(jié)果喂回去。核心循環(huán)的代碼量真的不多,難度主要在工程細節(jié)——參數(shù)解析、上下文管理、兜底策略、prompt 調(diào)優(yōu)。
如果你也想上手試試,建議別一開始就上 LangChain 那種重框架,先用原生 SDK 把 Agent 循環(huán)跑通,理解每一步在干什么。等你真的覺得手寫吃力了,再引入框架也不遲。
以上就是使用Python從零搭建一個能用的AI Agent的詳細內(nèi)容,更多關于Python實現(xiàn)AI Agent的資料請關注腳本之家其它相關文章!
相關文章
Python3實現(xiàn)爬取指定百度貼吧頁面并保存頁面數(shù)據(jù)生成本地文檔的方法
這篇文章主要介紹了Python3實現(xiàn)爬取指定百度貼吧頁面并保存頁面數(shù)據(jù)生成本地文檔的方法,涉及Python基于urllib模塊的頁面爬取與文件讀寫相關操作技巧,需要的朋友可以參考下2018-04-04
Python實現(xiàn)計算經(jīng)緯度坐標點距離的方法詳解
地球表面兩點間的距離計算看似簡單,實則涉及復雜的球面幾何,本文將用Python實現(xiàn)精確的球面距離計算,覆蓋從基礎公式到工程優(yōu)化的全流程,快跟隨小編一起學習一下吧2025-10-10
Python中easy_install 和 pip 的安裝及使用
本篇文章主要介紹了Python中easy_install 和 pip 的安裝及使用,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06
YOLOv5車牌識別實戰(zhàn)教程(四)模型優(yōu)化與部署
這篇文章主要介紹了YOLOv5車牌識別實戰(zhàn)教程(四)模型優(yōu)化與部署,在這個教程中,我們將一步步教你如何使用YOLOv5進行車牌識別,幫助你快速掌握YOLOv5車牌識別技能,需要的朋友可以參考下2023-04-04
Python實現(xiàn)復雜對象轉(zhuǎn)JSON的方法示例
這篇文章主要介紹了Python實現(xiàn)復雜對象轉(zhuǎn)JSON的方法,結(jié)合具體實例形式分析了Python針對json轉(zhuǎn)換的相關操作技巧,需要的朋友可以參考下2017-06-06

