一个 for 循环搞定 AI Agent:Function Calling + ReAct 实战
1.项目目的
一个能在终端里搜索网页、查天气、算数学、记住对话的研究助手。

2.最小原型
vbnet
普通 LLM: "特斯拉股价?" → "抱歉,我不知道实时数据。"
Agent: "特斯拉股价?"
→ LLM: 我需要 search_web 工具
→ 代码: 真的去搜网页
→ 结果喂回 LLM
→ "当前 313 美元,来源 TradingView"
Agent = LLM 决定做什么 + 你的代码真正去做。
3.核心代码
ReAct循环:Agent的核心
python
for step in range(max_steps):
# ---- Step 1: 调 LLM,告诉它有哪些工具可用 ----
response = client.chat.completions.create(
model = os.getenv("DEEPSEEK_MODEL","deepseek-v4-pro"),
messages = self.messages,
tools = [t.to_openai_schema() for t in self.tools.values()], # 所有工具描述
temperature = 0.3
)
msg = response.choices[0].message
# model_dump(): ChatCompletionMessage 对象 → dict,否则不能下标访问
self.messages.append(msg.model_dump())
# ---- Step 2: LLM 没说要调工具 → 这就是最终回答 ----
if not msg.tool_calls:
# ---- 短期记忆清理: 只保留干净的对话历史 ----
# 删掉 tool_calls 和 tool 结果等中间状态,
# 只留 System Prompt + 用户消息 + 不含 tool_calls 的助手消息
answer = msg.content or""
clean = []
for m in self.messages:
if m["role"] == "system":
clean.append(m)
elif m["role"] == "user":
clean.append(m)
elif m["role"] == "assistant" and not m.get("tool_calls"):
clean.append(m)
clean.append({"role":"assistant","content":answer})
self.messages = clean
return answer
# ---- Step 3: LLM 决定调工具 → 真正执行 ----
for tc in msg.tool_calls:
name = tc.function.name # 工具名,如 "search_web"
args = json.loads(tc.function.arguments) # 参数 JSON → dict
tool = self.tools.get(name) # 找到对应的 Tool 对象
# 防御性处理: 工具不存在 or 执行出错
if not tool:
result = f"工具 '{name}' 不存在"
else:
try:
result = tool.func(**args) # ← 真正执行函数!
except Exception as e:
result = f"工具执行失败: {e}"
# 执行结果作为 Tool 消息喂回 LLM
self.messages.append({
"role":"tool",
"tool_call_id":tc.id,
"content":str(result),
})
# 达到最大步数仍无最终回答 → 兜底返回
return "已达到最大步数,但仍未得到答案"
Tool类:让LLM读懂我的函数
python
class Tool:
"""
一个工具 = 名字 + 描述 + 参数规则 + 实际要调用的函数。
LLM 通过名字和描述决定"该不该调这个工具",
通过 parameters(JSON Schema 格式)知道"要传什么参数",
你的代码通过 func 真正执行。
"""
def __init__(self,name :str ,description : str,parameters :dict,func):
self.name = name # 工具名,如 "search_web"
self.description = description # 告诉 LLM 这个工具干什么
self.parameters = parameters # JSON Schema 格式的参数定义
self.func = func # 实际执行的 Python 函数
def to_openai_schema(self)->dict:
"""转成 OpenAI/DeepSeek API 要求的 tools 参数格式。
type: "function" + function: {name, description, parameters}"""
return {
"type":"function",
"function":{
"name":self.name,
"description":self.description,
"parameters":self.parameters,
}
}
4.自定义工具
| 工具 | 干什么 | 实现 |
|---|---|---|
search_web |
搜索网页 | Tavily API |
calculate |
算数学 | eval() |
get_weather |
查真实天气 | Open-Meteo API |
save_to_memory |
记住重要信息 | ChromaDB |
search_memory |
回忆之前的话 | ChromaDB |
长期记忆直接复用了RAG项目的 ChromaDB------写一遍的代码换了个场景再用。
5. 三个最有价值的坑
坑 1:LLM 假装调用了工具
sql
现象: LLM 回复"已保存!✅",但 save_to_memory 根本没被调用
学到: 没有硬约束时,LLM 会"表演"符合预期但没发生的回复。
System Prompt 加"当用户说记住时,你必须调用工具"。
坑 2:Agent 无限循环
makefile
现象: 搜了 5 次都不回答,每次都换关键词继续搜
学到: Prompt 工程 = Agent 的边界。加"最多搜 3 次,之后必须给出答案"。
坑 3:短期记忆清理
arduino
现象: 第二轮对话时 LLM 看到上轮的 tool_calls 垃圾,行为异常
学到: 消息历史要区分"该留的"和"该删的"------tool_calls 是中间状态,最终回答才留
6.收尾
1. 项目结构
bash
ai-research-agent/
├── .env # DeepSeek + Tavily API Key
├── requirements.txt # openai, tavily-python, chromadb, sentence-transformers...
└── src/
└── agent.py # 全部:Tool 类 + Agent 类 + 5 个工具 + CLI
2. 依赖关系
sql
agent.py
├── DeepSeek → 大脑(决策 + 工具选择)
├── Tavily → 手(搜索)
├── Open-Meteo → 手(天气)
├── ChromaDB → 记忆(长期存储,复用 Week 2)
└── sentence-transformers → 记忆的编码器
一个大脑 + 两只手 + 一个记忆库。
3. GitHub 链接
https://github.com/MRfyyt/ai-research-agent
4. 为什么不用 LangChain(整篇的落点)
"为什么不直接用 LangChain------它封装了 AgentExecutor,调两行 API 就行。但它的底层就是一个 for 循环:LLM 决定 → 执行工具 → 结果回传 → 循环。我用 30 行手写了一遍,现在看 LangChain 源码,每一行都能看懂它在干什么。先理解原理再学框架,比先学框架再理解原理快得多。 "