AI代码学习-Function Calling + ReAct

0 阅读14分钟

AI代码学习-Function Calling + ReAct

一、前篇回顾

文章从结构化输出的局限讲起:JSON 格式正确,并不代表内容可信,由此引出 Function Calling。它把“模型决定做什么”和“程序真正执行”分开。模型根据需求选择已注册工具并生成参数,宿主代码负责白名单校验、参数检查和函数调用,再把结果交回模型。文中使用 Agently 和搜索工具演示了“决策、执行、回答”的完整链路,并强调搜索候选只是 discovery_hint,不能直接当作事实。文章还说明了 Function Calling 与 ReAct Loop 的关系:前者处理工具描述、选择和参数生成,后者负责结果验证、失败重试、继续调用及停止判断。工具定义应写清使用场景和能力边界,准确设置参数类型,合理区分必填项与可选项。实际落地时,模型输出始终是不可信输入,权限、业务规则和最终执行权必须由宿主程序掌握。

二、先看完整调用流程

image-20260910170418470

之前的文章中完成了前半部分的代码示范,而后半部部分根据接口输出的结果判断是否符合用户要求是没有完成的。

这里我们先回顾下之前前半部分流程的逻辑,以便唤醒大家的记忆。

这里我们先将整个流程分为了三个阶段:

第一阶段为用户输入阶段:在这个阶段用户会在网页、客户端或 CLI 中输入提示词。

第二阶段为Agent处理阶段:咱们熟知的 LLM 推理就是在这里进行触发的,这里面触发的逻辑后面会在详细解释,大家只要知道在这阶段会通过各种手段帮助使用者得到预期的答案。

第三阶段为结果输出阶段:在这个阶段,Agent会将处理好的结果返回给网页、客户端或 CLI ,前段负责将内容通过流式或直接输出给用户。

以上三个阶段是整个流程的三个阶段,当然 Agent 内部在这个流程里面也是有几个阶段:

  • 提示词包装处理阶段:在这个阶段,通过代码会将 用户提示词、系统提示词、数据约束格式和针对任务,进行封装处理,处理后会将数据交给 LLM 进行推理。
  • 工具注册阶段:在启动 Agent 后,会先读取工具列表,将列表中工具注册入 LLM,在获得封装好的提示词后,有大模型进行推理,返回 LLM 需要调用的工具,格式为 JSON 格式,但是也是有例外这个主要是根据训练时候使用的数据,有时候外层会包裹一层引导性外壳,看到记得处理就好。
  • 触发工具阶段:在得到 LLM 返回选择的工具后,可以根据对应的 Action 调用对应的函数,在函数得到对应数据后,会将输出结果返回,这时会层层返回到调用方。
  • 循环阶段:在返回调用层后,就该将数据交给 LLM 进行验证,如果返回结果不符合预期,就重新返回再次调用函数,直到获取达到预期的数据。
  • 输出封装阶段:在得到预期数据后,就要根据与前端的协议将数据返回,最终展示在页面上。

根据上面描述的 Agent 内调用 Function Calling 和 ReAct 的五个阶段,可以清晰的了解到用户输入一次提示词后,内部都发生了什么,而且,在以后的开发过程中,按照这个流程至少不会迷茫自己该干什么。

到这里一次简单的Function Calling + ReAct 流程就走完了。

三、示例

results = []
    while True:
        tool_info_list = [
            {
                "name": tool_info["name"],
                "desc": tool_info["desc"],
                "args": tool_info["args"],
            }
            for tool_info in TOOL_LIST
        ]
        request = (
            Agently.create_request("react-function-command")
            .input({"用户目标": TASK})
            .info(
                {
                    "可用工具": tool_info_list,
                    "工具调用记录": results,
                }
            )
            .instruct(
                [
                    "请根据{可用工具}提供的信息,判断为了满足{用户目标},应该如何进行下一步\n"
                    "你可以选择使用工具,也可以选择直接回复\n",
                    "注意:\n"
                    "工具调用的时候,需要考虑到跨语种的情况,通常需要给出指令使用的语言和英文两类指令,可以生成多个工具调用指令\n",
                ]
            )
            .output(
                {
                    "next_step": (Literal["调用工具", "直接回复"],),
                    "tool_calls": (
                        [
                            {
                                "purpose": (str, "这次调用要补齐哪项信息", "not_null"),
                                "action_id": (
                                    "只能从{可用工具}中,选择具体的{name}",
                                    True,
                                ),
                                "args": (
                                    dict,
                                    "必须严格遵循{action_id}指定的工具名所对应的{可用工具.args}约束",
                                ),
                            }
                        ],
                        "如果{next_step}是调用工具,则需要有具体的工具调用指令列表,否则保持为[]",
                    ),
                    "reply": (
                        str,
                        "如果{next_step}是直接回复,则必须有具体的回复内容,否则保持为''",
                    ),
                },
                format="json",
            )
        )
        command = request.get_data()
        print("[指令判断]", command)
        if command["next_step"] == "直接回复":
            print(command["reply"])
        else:
            for tool_call in command["tool_calls"]:
                tool_name = tool_call["action_id"]
                tool_dict = {tool_info["name"]: tool_info for tool_info in TOOL_LIST}
                if tool_name in tool_dict:
                    result = tool_dict[tool_name]["func"](**tool_call.get("args", {}))  # type: ignore
                    results.append({tool_call["purpose"]: result})

            final_answer = (
                Agently.create_request("react-function-answer")
                .input({"user_goal": TASK})
                .info(
                    {
                        "可用工具": tool_info_list,
                        "历史工具调用结果": results,
                    }
                )
                .instruct(
                    [
                        "根据{tool_call_output}的结果,判断是否能够支持对{user_goal}进行回复,还是需要进行下一轮工具调用",
                        "注意:不允许只根据概要信息就进行回复,在有可以进一步观察的URL等信息时,务必通过浏览等工具能力获取全量信息后再回复",
                    ]
                )
                .output(
                    {
                        "can_reply": (
                            bool,
                            "判断是否可以直接回复,True-可以直接回复,False-需要进行下一轮工具调用",
                        ),
                        "reply": (
                            str,
                            "如果{can_reply}为True,在这里给出你的具体回复,否则保持为空",
                        ),
                    }
                )
                .get_data()
            )
            print("[本轮结果]", final_answer)
            if final_answer["can_reply"]:
                print(final_answer["reply"])
                break

四、输出日志

[指令判断] {'next_step': '调用工具', 'tool_calls': [{'purpose': '搜索中文网页中关于 NVIDIA 2026 年机器人进展的信息', 'action_id': 'search_web', 'args': {'query': 'NVIDIA 2026 年机器人进展'}}, {'purpose': '搜索英文网页中关于 NVIDIA 2026 robotics progress 的信息', 'action_id': 'search_web', 'args': {'query': 'NVIDIA 2026 robotics progress'}}, {'purpose': '搜索英文网页中关于 NVIDIA 2026 robotics roadmap 或 humanoid robot 的信息', 'action_id': 'search_web', 'args': {'query': 'NVIDIA 2026 robotics roadmap humanoid robot'}}], 'reply': ''}
[INFO] 2026-10-09 15:21:22 | response: https://yandex.com/search/site/?text=NVIDIA+2026+%E5%B9%B4%E6%9C%BA%E5%99%A8%E4%BA%BA%E8%BF%9B%E5%B1%95&web=1&searchid=3716541 200
[INFO] 2026-10-09 15:21:24 | response: https://yandex.com/search/site/?text=NVIDIA+2026+robotics+progress&web=1&searchid=2063986 200
[INFO] 2026-10-09 15:21:25 | response: https://yandex.com/search/site/?text=NVIDIA+2026+robotics+roadmap+humanoid+robot&web=1&searchid=4872813 200
[本轮结果] {'can_reply': False, 'reply': ''}
  [指令判断] {'next_step': '调用工具', 'tool_calls': [{'purpose': '浏览英文候选文章,确认 NVIDIA 2026 年在物理 AI、人形机器人、机器人平台或自动驾驶机器人方面的具体进展', 'action_id': 'browse_page', 'args': {'url': 'https://arstechnica.com/ai/2026/10/nvidias-big-bet-on-physical-ai-aims-for-safer-robotaxis-humanoid-robots/'}}, {'purpose': '浏览英文候选文章,获取 2026 年 NVIDIA 在人形机器人和物理 AI 测试、部署或路线图方面的补充信息', 'action_id': 'browse_page', 'args': {'url': 'https://www.tomorrowtested.com/en/news/tomorrow-tested-physical-ai-humanoid-robotics-2026/'}}, {'purpose': '使用中文检索词补充搜索 NVIDIA 2026 年机器人进展,覆盖中文报道、官方信息和人形机器人相关内容', 'action_id': 'search_web', 'args': {'query': 'NVIDIA 2026 人形机器人 进展'}}, {'purpose': '使用英文检索词补充搜索 NVIDIA 2026 robotics progress,覆盖 CES 2026、Isaac、GR00T、Physical AI 等关键词', 'action_id': 'search_web', 'args': {'query': 'NVIDIA 2026 robotics progress Isaac GR00T Physical AI'}}], 'reply': ''}
[INFO] 2026-10-09 15:22:05 | response: https://yandex.com/search/site/?text=NVIDIA+2026+%E4%BA%BA%E5%BD%A2%E6%9C%BA%E5%99%A8%E4%BA%BA+%E8%BF%9B%E5%B1%95&web=1&searchid=1415543 200
[INFO] 2026-10-09 15:22:07 | response: https://yandex.com/search/site/?text=NVIDIA+2026+robotics+progress+Isaac+GR00T+Physical+AI&web=1&searchid=7962420 200
[本轮结果] {'can_reply': False, 'reply': ''}
[指令判断] {'next_step': '调用工具', 'tool_calls': [{'purpose': '补齐英文候选文章中关于 NVIDIA 2026 年物理 AI、人形机器人、机器人平台或自动驾驶机器人进展的具体内容', 'action_id': 'browse_page', 'args': {'url': 'https://www.tomorrowtested.com/en/news/tomorrow-tested-physical-ai-humanoid-robotics-2026/'}}, {'purpose': '使用英文检索词补充搜索 NVIDIA 2026 年机器人进展,覆盖 CES 2026、Isaac、GR00T、Jetson Thor、Physical AI 等关键线索', 'action_id': 'search_web', 'args': {'query': 'NVIDIA 2026 robotics progress CES Isaac GR00T Jetson Thor Physical AI'}}, {'purpose': '使用中文检索词补充搜索 NVIDIA 2026 年机器人进展,覆盖官方信息、中文报道、人形机器人和物理 AI 落地情况', 'action_id': 'search_web', 'args': {'query': 'NVIDIA 2026 机器人进展 CES Isaac GR00T Jetson Thor 物理AI'}}], 'reply': ''}
[INFO] 2026-10-09 15:22:42 | response: https://yandex.com/search/site/?text=NVIDIA+2026+robotics+progress+CES+Isaac+GR00T+Jetson+Thor+Physical+AI&web=1&searchid=1590117 200
[INFO] 2026-10-09 15:22:43 | response: https://yandex.com/search/site/?text=NVIDIA+2026+%E6%9C%BA%E5%99%A8%E4%BA%BA%E8%BF%9B%E5%B1%95+CES+Isaac+GR00T+Jetson+Thor+%E7%89%A9%E7%90%86AI&web=1&searchid=9219993 200
[本轮结果] {'can_reply': False, 'reply': ''}
[指令判断] {'next_step': '调用工具', 'tool_calls': [{'purpose': '补齐中文语境下 NVIDIA 2026 年机器人进展,重点覆盖 CES 2026、Isaac、GR00T、Jetson Thor、Physical AI 等关键线索', 'action_id': 'search_web', 'args': {'query': 'NVIDIA 2026 机器人进展 CES Isaac GR00T Jetson Thor 物理AI'}}, {'purpose': '补齐英文语境下 NVIDIA 2026 robotics progress,重点覆盖 CES 2026、Isaac、GR00T、Jetson Thor、Physical AI、humanoid robot 等关键线索', 'action_id': 'search_web', 'args': {'query': 'NVIDIA 2026 robotics progress CES Isaac GR00T Jetson Thor Physical AI humanoid robot'}}, {'purpose': '浏览中文候选文章,确认 NVIDIA 在 CES 2026 发布的实体 AI 模型、机器人平台、全球合作伙伴及新一代机器人进展', 'action_id': 'browse_page', 'args': {'url': 'https://makerpro.cc/nvidia-releases-new-physical-ai-models-as-global-partners-unveil-next-generation-robots/'}}, {'purpose': '重新浏览英文候选文章,补齐此前被截断的 NVIDIA 2026 物理 AI 与人形机器人相关段落,获取具体进展、测试、部署或路线图信息', 'action_id': 'browse_page', 'args': {'url': 'https://www.tomorrowtested.com/en/news/tomorrow-tested-physical-ai-humanoid-robotics-2026/'}}, {'purpose': '浏览英文候选文章,确认 Jetson Thor 在 2026 年 NVIDIA 机器人/物理 AI 进展中的定位、落地信号或应用场景', 'action_id': 'browse_page', 'args': {'url': 'https://www.neuealchemy.io/signal-noise/when-physical-meets-digital-what-jetson-thor-really-signals-field-report'}}, {'purpose': '浏览英文候选文章,确认 NVIDIA 与 LG在人形机器人、Vera Rubin AI 工厂或机器人基础设施方面的 2026 合作进展', 'action_id': 'browse_page', 'args': {'url': 'https://pccentral.net/lg-nvidia-partner-humanoid-robots-vera-rubin-ai-factories/'}}, {'purpose': '浏览英文候选文章,确认 NVIDIA 与 Hugging Face 在机器人开源模型、开发工具或小型开发团队支持方面的 2026 进展', 'action_id': 'browse_page', 'args': {'url': 'https://www.ai-jarvis.eu/nvidia-and-hugging-face-join-forces-robotics-gets-open-source-models-even-small-development-teams'}}], 'reply': ''}
[INFO] 2026-10-09 15:23:25 | response: https://yandex.com/search/site/?text=NVIDIA+2026+%E6%9C%BA%E5%99%A8%E4%BA%BA%E8%BF%9B%E5%B1%95+CES+Isaac+GR00T+Jetson+Thor+%E7%89%A9%E7%90%86AI&web=1&searchid=3624504 200
[INFO] 2026-10-09 15:23:27 | response: https://yandex.com/search/site/?text=NVIDIA+2026+robotics+progress+CES+Isaac+GR00T+Jetson+Thor+Physical+AI+humanoid+robot&web=1&searchid=8484442 200
[本轮结果] {'can_reply': False, 'reply': ''}

五、代码及输入日志分析

5.1 代码链路对照

先把示例代码和第二节的"Agent 内部五阶段"对应起来,方便理解每一行代码在流程中的位置:

阶段代码位置说明
工具注册tool_info_list 推导每次循环都把 TOOL_LIST 中工具的 name/desc/args 喂给 LLM,相当于动态注册
提示词包装Agently.create_request("react-function-command")input 传入用户目标,info 传入可用工具和历史调用记录,instruct 约束跨语种策略,output 约束结构化输出
触发工具tool_dict[tool_name]["func"](**args)白名单校验后执行,结果以 {purpose: result} 追加进 results
循环(ReAct 验证)react-function-answer 请求第二个 LLM 调用专门负责判断"能不能回复了",不能直接回复就进入下一轮
输出封装print(final_answer["reply"]) + break满足条件后退出循环,把答案交给上层

这里有一个设计上的关键点:决策和验证是两次独立的 LLM 调用。react-function-command 只管"下一步干什么",react-function-answer 只管"信息够不够了"。职责拆开之后,每个请求的提示词都能写得很聚焦,输出结构也简单,这是比"一个大提示词包打天下"更稳的做法。

另外注意 instruct 里的这条约束:"不允许只根据概要信息就进行回复,在有可以进一步观察的URL等信息时,务必通过浏览等工具能力获取全量信息后再回复"。这条约束直接决定了验证器的严格程度,也是后面日志里循环迟迟不收敛的直接原因。

5.2 日志逐轮分析

日志一共跑了 4 轮,can_reply 全部为 False,没有收敛。逐轮看:

第 1 轮:模型给出 3 个搜索指令,覆盖中英文("NVIDIA 2026 年机器人进展"、"NVIDIA 2026 robotics progress"、"robotics roadmap humanoid robot")。跨语种并行的策略生效了,搜索全部返回 200。但验证器判断:搜索结果只是概要(呼应前篇的 discovery_hint 概念),不足以回复。

第 2 轮:模型开始尝试 browse_page 抓取候选文章全文(arstechnica、tomorrowtested),同时补了 2 个搜索。注意一个细节:日志里只有 search_web 的 INFO 记录,看不到任何 browse_page 的输出。这说明要么 browse_page 没有打日志,要么它的结果没有有效返回给模型。

第 3 轮:模型再次请求 tomorrowtested 同一个 URL(purpose 里明确写了"补齐此前被截断的段落"),说明第 2 轮的抓取结果确实不理想——内容被截断或为空。同时搜索关键词开始带上 CES、Isaac、GR00T、Jetson Thor 等从前几轮结果中学到的实体词,这是 ReAct 循环"越搜越准"的正面体现。

第 4 轮:指令数量膨胀到 7 个,其中 tomorrowtested 被第三次请求,又新增了 makerpro、neuealchemy、pccentral、ai-jarvis 四个站点的抓取。验证器依然判定信息不足。

5.3 暴露的问题

从这份日志里能提炼出几个典型问题,这也是 Function Calling + ReAct 落地时几乎必踩的坑:

  1. 工具失败对模型不可见。browse_page 抓取被截断/失败后,模型只能从"结果不够用"间接推断,于是反复重试同一个 URL。正确的做法是工具层把失败原因(超时、内容为空、被反爬)显式写进返回值,让模型知道"这条路走不通,换一条",而不是盲目重试。

  2. 没有循环退出保护。while True 只依赖 can_reply 跳出,一旦验证器持续严格(本例就是 instruct 写得太严)或工具持续失败,就是死循环。生产环境必须加 max_iterations 上限,超限后带着已有信息降级回复。

  3. 没有调用去重。同一个 URL 被抓取 3 次,相似语义的搜索词反复出现。可以在执行前对 action_id + args 做哈希去重,已调用过的直接复用历史结果。

  4. "直接回复"分支有漏洞。代码里 if command["next_step"] == "直接回复" 只做了 print,没有 break,模型一旦选择直接回复,循环照样继续跑。这是一个真实的 bug。

  5. results 的 key 设计不合理。results.append({tool_call["purpose"]: result}) 用自然语言描述当 key,语义相近的 purpose 会被当成不同记录,不利于去重和回溯。更稳妥的结构是 {action_id, args, result} 三元组。

  6. 每轮重建静态数据。tool_info_list 和 tool_dict 在循环内每轮都重新构造,功能上没错但属于浪费,移到循环外即可。

5.4 小结

这份日志其实是一次非常有价值的"失败样本":跨语种并行搜索、实体词逐步细化这些 ReAct 的正面特性都体现出来了,但工具失败不透明、缺少退出保护这两个问题叠加,导致循环空转了 4 轮。这也再次印证前篇的结论——模型输出始终是不可信输入,循环的控制权、工具的成败语义、最终的停止判断,都必须由宿主程序掌握,不能指望模型自己"想明白"什么时候该停。

结束语

到这里,Function Calling + ReAct 的完整链路就走完了:从三阶段的宏观流程,到 Agent 内部的五个阶段,再到一份真实运行日志暴露出的种种问题。回头看,这套机制的本身并不复杂——模型负责"想",程序负责"做",中间用结构化输出衔接,用循环兜底。真正拉开差距的是工程细节:工具描述写得清不清楚、失败语义透不透明、循环有没有刹车,这些才是决定 Agent 可用性的关键。

但同时也要看到,本文的示例里工具是写死在 TOOL_LIST 里的,注册、查找、调用、结果汇总全是手工代码。当工具数量从三五个涨到三五十个,这种方式就撑不住了:工具怎么统一管理、LLM 怎么按需读取工具描述、调用权限怎么控制、多轮结果怎么汇总,都需要一套更系统的机制。

下一篇《管理 Agent 行动能力-注册、读取、调用和汇总》,咱们就来解决这些问题,把"能用工具"升级成"会管理工具"。