在產品開發中整合 LLM API 就能說自己有用 Agent 了嗎?
最近在開發的產品需要呼叫 LLM API 去解析一些複雜的 content,但所有流程都是確定的,A 做完後做 B,失敗的話做 C 等等,依據一套預先定義好的規則行動,不會有意外,如果真的有意外,那就是程式沒寫好,看 fallback 怎麼處理了。偏偏老闆一直堅持這就是 Agent,那我也沒辦法,你說的都對。
根據 Anthropic 在 Building effective agents 中對 Agent 的定義,如果 LLM 沒有控制 workflow execution,例如單輪 LLM、classifier、普通 chatbot,就不算 Agent;Agent 會用 LLM 管理 workflow execution、動態選 Tool,並判斷何時完成、失敗時如何修正。
光譜上大概是這樣吧:
Workflow control by model / Agent autonomy
低 ───────────────────────────────────────────────────────────────→ 高
Predefined Workflow Hybrid / Agent-assisted Agent
│ │ │
流程由程式決定 程式與模型共同決定流程 模型動態決定流程
其實大部分的任務本來就比較適合 Predefined Workflow,尤其是那些本來就有標準流程的,我們能用 AI 替換掉其中的某些比較困難或耗時的任務,光是這樣其實也已經夠有價值了。
畢竟網路上已經有很多文章寫到爛掉了,有點不值一提,直接請 GPT 做一張介紹圖吧。

白話來說就是讓 LLM 想一步做一步看一步,應該是最簡單也是最廣為人知的架構,只要 Tool Calling 再加一個 loop 就可以做一個簡單的 Agent 了。
優缺點分明啊,他很適合走完一步才知道下一步要做什麼的任務,舉裡來說我跟 Agent 說我想查 DB 壞掉的原因,Agent 毫無頭緒啊,很難先規劃接下來的每一步。但有幾個明顯得缺點,第一點就是邊走邊想一定比較慢,第二點是比較沒有一個全局規劃,沒有 checklist 可能會被最新的結果帶跑,第三點比較嚴重,就是錯誤會累積,假設中間有一步做錯了沒被抓出來,那後面就是在錯誤的基礎上繼續跑。
先把所有步驟都規劃好,再逐步執行,跟 ReAct 算是完全反過來吧,實務上其實會兩個搭配使用,先做全局 Plan 之後交給 ReAct 去決定執行細節。
概念上是把 ReAct 的 Observe 的階段省略掉,但實際上比較像 Plan-and-Execute。
跟 Plan-and-Execute 最大的不同的是,ReWOO 在規劃階段就把後續需要哪些 Evidence、Tool 以及彼此 dependency 一起寫出來,盡量避免每次 Tool 執行後都重新找 LLM reasoning。Plan-and-Execute 則沒有這麼明確的限制,想怎麼設計都行,可以說 ReWOO 是更強調 Tool dependency 與減少 LLM call 的 Plan-and-Execute。
一句話就是,研究好夥伴,我的免費牛馬,給我試到我滿意為止。
雖然有很多現成的 Agent 的框架可以用,但自己寫一下也是挺有趣。
就像前面說的,ReAct 就是 Tool Calling 再加一個 loop,來看看究竟是不是。
LLM 只會打嘴砲,要讓 agent 做事要先給他做事的能力,基本上只要回傳的是文字都行。
def get_full_name(short_name) -> str:
short_name = short_name.lower()
names = {
"emma": "Emma Chen",
"jack": "Jack Liu"
}
return names.get(short_name, f"找不到 {short_name} 的全名")
def get_phone(full_name: str) -> str:
phones = {
"Emma Chen": "0912-345-678",
"Jack Liu": "0988-765-432"
}
return phones.get(full_name, f"找不到 {full_name} 的電話")
TOOLS: dict[str, Callable[..., str]] = {
"get_full_name": get_full_name,
"get_phone": get_phone
}
class Agent:
def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> str:
tool_name = tool_name.strip()
tool = TOOLS.get(tool_name)
if tool is None:
available = ", ".join(sorted(TOOLS))
return f"Unknown tool {tool_name!r}. Available tools: {available}."
try:
return str(tool(**arguments))
except Exception as exc:
return f"Tool {tool_name!r} failed: {exc}"
接著參考 IBM 的這篇文章來寫一下 prompt:
SYSTEM_PROMPT = """
You have access to the following tools:
- get_full_name(short_name: str) -> returns the user's full name,
- get_phone(full_name: str) -> returns that person's phone,
Use the following format:
Question: the input question you must answer.
Thought: you should always think before you take action.
Action: the action to take, should be one of [get_full_name, get_time, get_phone], and an json string for input, splited with ",", for example: get_full_name, {"short_name":"Emma Chen"}
Observation: What I will give you for the action result.
... (this Thought/Action/Observation can repeat N times, you should strictly follow the order of these 3 moves)
Thought: you now know the final answer
Final Answer: the final answer to the original input question
Question and Observation are inputs.
Thought, Action and Final Answer are your outputs, only output one from [Thought, Action, Final Answer] at a time.
Output final answer only when you can answer the Question.
"""
這裡故意不去設定 tool 和 text.format,看看能不能單純靠 PROMPT 來控制 Thought → Action → Observation 的流程。
class Agent(Agent):
def get_response(self, text: str):
return client.responses.create(
model="gpt-6-astra",
input=text,
instructions=SYSTEM_PROMPT
)
def start_task(self, input: str) -> str:
content = f"Question: {input}"
print(content)
for _ in range(30):
response = self.get_response(content)
output_text = response.output_text.strip()
content += output_text
print(output_text)
if "Final Answer:" in output_text:
return
elif output_text.startswith("Action:"):
action, action_input = output_text[len("Action:") :].split(",")
action = action.strip()
action_input = json.loads(action_input.strip())
observation = self.call_tool(action, action_input)
next_input = f"Observation: {observation}"
content += next_input
print(next_input)
elif output_text.startswith("Thought:"):
next_input = output_text
else:
next_input = "Observation: Your output should starts with Thought, Action or Final Answer."
content += next_input
print(next_input)
raise RuntimeError(f"Agent exceeded max_steps={self.max_steps}.")
流程的確是完全照著設定好的 Pattern 走。
是不是覺得 Thought 在這裡很雞肋?的確這種簡單的任務好像根本不用思考,但假設是複雜點的任務,強迫模型思考是可以減少幻覺的,另外也有助於提升整個 workflow 的透明度。
agent = Agent()
agent.start_task("jack 和 kevin 的電話分別是?")
# Question: jack 和 kevin 的電話分別是?
# Thought: 我需要先查詢 Jack 和 Kevin 的全名,才能查找各自的電話。
# Action: get_full_name, {"short_name":"jack"}
# Observation: Jack Liu
# Thought: 接著查詢 Jack Liu 的電話。
# Action: get_phone, {"full_name":"Jack Liu"}
# Observation: 0988-765-432
# Thought: 接著需要查詢 Kevin 的全名,才能查找他的電話。
# Action: get_full_name, {"short_name":"kevin"}
# Observation: 找不到 kevin 的全名
# Final Answer: Jack Liu 的電話是 0988-765-432。目前找不到 Kevin 的全名,因此無法查詢他的電話;請提供 Kevin 的全名。
但你如果我換個方式問,像是:
agent.start_task("你認識Jack嗎?你知道怎麼連絡他嗎?他還欠我錢欸")
# Question: 你認識Jack嗎?你知道怎麼連絡他嗎?他還欠我錢欸
# Thought: 我需要先確認你指的是哪位 Jack,才能查詢他的聯絡方式。
# Action: get_full_name, {"short_name":"Jack"}
# Observation: Jack Liu
# Thought: 目前查到的姓名是 Jack Liu,接下來需要確認是否有他的聯絡方式。
# Action: get_phone, {"full_name":"Jack Liu"}
# Observation: 0988-765-432
# Final Answer: 根據你提供的查詢結果,Jack 的全名是 Jack Liu,電話是 0988-765-432。我不認識他本人,也無法確認號碼是否仍有效;聯絡時建議先確認身分,再平和地詢問還款安排。
雖然結果還是成功取得資訊,但顯然 Jack 完蛋了,為了守護 Jack 的錢包我們勢必得要做更多控制才行,單純自己寫挺麻煩的,所以是時候讓明天的我去試試框架了。
不是 Agent 沒關係,簡報上是就好。