一个 for 循环搞定 AI Agent:Function Calling + ReAct 实战

一个 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 源码,每一行都能看懂它在干什么。先理解原理再学框架,比先学框架再理解原理快得多。 "

相关推荐
正在走向自律1 小时前
AI 辅助研发内部复盘(4/5):约束、上下文与技能——把“人的判断”工程化
人工智能·ai编程·ai代码生成·上下文工程·ai辅助研发·skills封装经验
Bigger1 小时前
因为一句「华流才是最屌的」,我做了一个中国风设计的 Skill
前端·人工智能·设计
陈天伟教授1 小时前
图解人工智能(99)人工智能前沿-走向未来
人工智能
科研小刘带你玩学术1 小时前
【科研快讯】从自动执行到自主决策:具身智能正在推动AI进入机器人智能时代
人工智能·机器学习·具身智能·未来趋势·ai机器人·智能系统
ACP广源盛139246256731 小时前
GSV6155@ACP# Type-C 视频信号转换芯片:技术架构、工程痛点适配与落地场景分析
大数据·c语言·开发语言·人工智能·分布式·嵌入式硬件·架构
MartinYeung51 小时前
[论文学习]Skill-MAS:面向自动多智能体系统的元技能进化
大数据·人工智能·学习
workflower2 小时前
高质量数据集的类型
人工智能·机器学习·设计模式·自然语言处理·机器人
aneasystone本尊2 小时前
学习 Headroom 的压缩管线
人工智能
wabs6662 小时前
关于文献【MLEng/MLOps】
人工智能·文献