news 2026/7/2 5:06:25

《8天Java后端工程师转AI Agent》Day 1:手写第一个 ReAct 单 Agent(不上框架)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《8天Java后端工程师转AI Agent》Day 1:手写第一个 ReAct 单 Agent(不上框架)

这是「8天Java后端工程师转AI Agent」系列的第二篇。上一篇(Day 0)把环境和第一次 API 调用跑通了:https://blog.csdn.net/ASIA_kobe/article/details/161839219

我是一个工作8年的Java工程师,之前所有的工作都在 JVM、分布式、服务治理、中间件这一层。这个系列记录我从零开始、把 AI Agent 从概念学到能跑出一个自己用得上的小工具的全过程。


一、这一篇要跑通什么

一句话:让模型自己决定"我需要去查数据",调一个工具拿到真实数据,再基于数据回答。

对比一下你就懂它的价值了:

  • 普通聊天:你问"AAPL 现在估值怎么样",模型凭训练时的记忆随口给你一个数——大概率是编的、过时的。
  • Agent:模型意识到"我不知道最新价格",主动调一个get_quote工具去拿真实数据,拿到后再回答。

后者就是ReAct。这一篇我们不上任何框架,用几十行 Python 手写这个循环。手写一遍,是你后面用任何框架(LangGraph 之类)的底气——因为你知道框架在帮你藏掉什么。


二、ReAct 是什么

ReAct =Reasoning + Acting,来自 2022 年的论文《ReAct: Synergizing Reasoning and Acting in Language Models》。

核心是一个循环:

用户提问 ↓ [推理 Reason] 模型想:"我需要 AAPL 的最新价,光靠记忆不行" ↓ [行动 Act] 模型决定调用 get_quote("AAPL") ↓ [观察 Observe] 拿到工具返回 {price: 195.3, pe: 31.2, ...} ↓ [再推理] 模型想:"数据够了,可以回答了" ↓ 最终答案

之前的模型要么"光想不做"(思维链,但不能调工具),要么"做但不想"(直接生成一个调用,没有推理)。ReAct 第一次把两者交替起来。

对后端工程师的类比:这就是一个while循环里的状态机——每一轮要么"继续调工具",要么"结束返回"。你天天写这种东西。


三、几个必须先建立的概念

在看代码前,先把 4 个词对齐,不然代码看不懂。

1. Tool / Function Calling

你把工具的「名字、用途、参数」用 JSON Schema 描述给模型。模型推理时如果决定要调,会返回一个结构化的 tool_call 对象(不是在文本里写"请帮我调 xx"),里面有函数名和参数。

关键认知:模型并不是"真的调用"了什么。它只是生成了一个"我想调这个函数、传这些参数"的请求,然后你的代码去真正执行,再把结果塞回去。模型全程碰不到你的函数。

对后端工程师:工具就是"带自然语言文档的接口定义"。你天天在干这事,只不过这次文档是给模型读的。

2. Messages 数组(对话状态机)

模型是无状态的——每次 API 调用都要把完整对话历史重传一遍。messages数组就是这个状态机,里面有 4 种角色:

role谁说的例子
system你给模型的指令“你是研究助手…”
user用户输入“AAPL 怎么样?”
assistant模型回复(可能含 tool_calls)“我要调 get_quote”
tool工具执行结果{price: 195.3, ...}

Agent 的所有"记忆"和"上下文",物理上就是这个数组。

3. tool_call_id

模型一次推理可以并行返回多个工具调用,每个有独立的id。把工具结果塞回 messages 时,必须用对应的 id 配对,否则模型分不清"这个返回是哪次调用的结果"。这是手写 ReAct 最容易写错的细节。

4. max_iter(最大循环次数)

模型可能死循环调工具(参数错→失败→换参数→还失败…)。max_iter是兜底的保险丝。简单任务 3–5 够用。


四、完整代码

先准备好一个假的行情工具(这一篇专注理解循环,不接真 API):

""" Day 1: Hand-rolled ReAct single agent. Core loop: reason -> call tool -> observe result -> reason again """importjsonfromopenaiimportOpenAI client=OpenAI(api_key="你的key",base_url="你的endpoint",# 用官方就删掉这行)MODEL="gpt-4o-mini"# 或你 endpoint 支持的模型名# ====== 工具实现(假数据,专注看循环) ======defget_quote(ticker:str)->dict:"""返回某标的的行情类指标(模拟数据)"""fake_db={"AAPL":{"price":195.3,"pe":31.2,"change_1y_pct":24.5,"currency":"USD"},"TSLA":{"price":178.6,"pe":65.4,"change_1y_pct":-8.3,"currency":"USD"},"NVDA":{"price":1180.0,"pe":72.1,"change_1y_pct":198.0,"currency":"USD"},}key=ticker.upper()ifkeyinfake_db:return{"ticker":key,**fake_db[key]}return{"error":f"unknown ticker:{ticker}"}

工具的「接口契约」——这段 schema 就是给模型看的接口文档:

TOOLS=[{"type":"function","function":{"name":"get_quote","description":("Fetch the latest quote for an asset: price, P/E ratio, ""and 1-year percentage change. ""Ticker formats: US assets use the bare symbol (e.g. AAPL, NVDA)."),"parameters":{"type":"object","properties":{"ticker":{"type":"string","description":"Ticker symbol, e.g. AAPL"},},"required":["ticker"],},},}]TOOL_IMPL={"get_quote":get_quote}# 工具名 -> 真实函数

核心:ReAct 主循环:

defrun_agent(user_query:str,max_iter:int=5)->str:messages=[{"role":"system","content":("You are a research assistant. ""Whenever you need price, PE, or performance data, you MUST call ""the get_quote tool instead of relying on memory. ""Keep final answers concise and structured."),},{"role":"user","content":user_query},]forstepinrange(1,max_iter+1):print(f"\n========== iteration{step}==========")resp=client.chat.completions.create(model=MODEL,messages=messages,tools=TOOLS,temperature=0.2,)msg=resp.choices[0].message# 情况 A:模型不再调工具 -> 给出最终答案,退出循环ifnotmsg.tool_calls:print("[final answer produced]")returnmsg.contentor""# 情况 B:模型要调工具 -> 执行 + 把结果喂回去messages.append(msg.model_dump(exclude_unset=True))forcallinmsg.tool_calls:name=call.function.name args=json.loads(call.function.arguments)print(f"[tool call]{name}({args})")impl=TOOL_IMPL.get(name)result=impl(**args)ifimplelse{"error":f"unknown tool:{name}"}print(f"[tool result]{result}")# 工具结果以 role=tool 的消息塞回,用 tool_call_id 配对messages.append({"role":"tool","tool_call_id":call.id,"content":json.dumps(result,ensure_ascii=False),})return"(max iterations reached; possible loop)"if__name__=="__main__":question="How does AAPL look right now in terms of valuation and 1-year performance? Give a short take."print(f"User query:{question}")answer=run_agent(question)print("\n========== final answer ==========")print(answer)

五、跑一遍,看清每一步

运行输出(关键在过程,不在最终文字):

User query: How does AAPL look right now ... ========== iteration 1 ========== [tool call] get_quote({'ticker': 'AAPL'}) [tool result] {'ticker': 'AAPL', 'price': 195.3, 'pe': 31.2, 'change_1y_pct': 24.5, 'currency': 'USD'} ========== iteration 2 ========== [final answer produced] ========== final answer ========== Here's the quick snapshot on Apple: - Price: $195.30 - P/E: 31.2x — a premium multiple, above the S&P 500 average - 1-Year Change: +24.5% — outpacing the broader market ...

看清楚发生了什么:

第 1 轮:模型推理 -> 决定调 get_quote("AAPL") -> 工具返回真实数据 第 2 轮:模型基于工具结果 -> 不再调工具 -> 给出最终答案

三个关键观察:

  1. 模型没有凭记忆瞎编——它没有直接说"AAPL 大概 200 块",而是先调了工具。
  2. 循环自然停止——第 2 轮模型自己判断"数据够了",不再返回 tool_calls,循环结束。这个"模型自己决定何时停"就是 Agent 的自主性,也是它和写死步骤的普通脚本的本质区别。
  3. 整个 Agent 就这几十行——没有框架,全是你自己写的。你现在完全看得清循环里发生的每一步。

六、自己动手玩这几个变体(比看十遍都管用)

亲手改一下,体感立刻不一样:

1. 问一个数据库里没有的标的

question="How does MSFT look?"# 假数据库里没有 MSFT

看模型怎么处理error返回——是老实说"查不到",还是硬编一个数?

2. 问一个需要连续调两次的问题

question="Compare AAPL and NVDA — which looks more reasonably valued?"

看模型会不会一次并发调两个工具——这是 ReAct 真正"活起来"的瞬间。

3. 故意把工具描述写差
description改成只写"Get stock data.",看模型会不会传错参数格式、或者干脆不调。这能让你直观感受tool description 是 prompt engineering 的隐藏战场

4. 问一个根本不需要工具的问题

question="What is a P/E ratio?"

看模型有没有自知之明——直接回答,而不是画蛇添足去调工具。


七、Day 1 关键词速查

关键词一句话
ReAct推理 → 行动(调工具)→ 观察 → 再推理,直到给答案
Function Calling模型返回"想调哪个函数、传什么参",你的代码去真正执行
Tool Schema工具的自然语言接口契约(name + description + 参数)
Messages 数组Agent 的全部记忆和上下文,物理上就是这个列表
tool_call_id工具结果塞回时和调用配对,不能错
max_iter死循环的兜底保险丝
自主性模型自己决定"要不要调工具、什么时候停"——Agent 区别于普通脚本的本质

八、下一篇预告:Day 2 - 多工具与筛选

Day 1 只有一个工具,模型没得选。Day 2 挂上 3 个工具(行情、基本面、新闻),让模型在多个工具间正确选择、组合调用,完成一个"单工具搞不定"的筛选任务——比如"帮我筛出基本面达标、且近期没有重大利空的标的"。

到时候你会第一次看到模型一次并发触发 10 个工具调用,也会第一次直观感受到"对话历史滚雪球、token 成本翻倍"这个后面要反复对付的问题。


配套阅读

  • ⭐ ReAct 原始论文(Yao et al., 2022)— https://arxiv.org/abs/2210.03629
    看懂那张"推理+行动"交替的图,核心思想就到手了。
  • ⭐ Lilian Weng《LLM Powered Autonomous Agents》— https://lilianweng.github.io/posts/2023-06-23-agent/
    Agent 领域最经典的综述,「规划 / 记忆 / 工具使用」三大件讲得最透。

系列后续更新于:https://blog.csdn.net/ASIA_kobe?type=blog
欢迎同样在转 AI 路上的同行点关注,我们一起把这 8 天走完。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/2 5:05:26

如何3分钟搞定OFD转PDF:完全免费的开源工具Ofd2Pdf终极指南

如何3分钟搞定OFD转PDF:完全免费的开源工具Ofd2Pdf终极指南 【免费下载链接】Ofd2Pdf Convert OFD files to PDF files. 项目地址: https://gitcode.com/gh_mirrors/ofd/Ofd2Pdf 还在为OFD文件打不开而烦恼吗?每次收到电子发票、政府公文或重要合…

作者头像 李华
网站建设 2026/7/2 5:05:03

NcmpGui:极速解锁网易云音乐NCM格式,让音乐真正属于你

NcmpGui:极速解锁网易云音乐NCM格式,让音乐真正属于你 【免费下载链接】ncmppGui 一个使用C编写的极速ncm转换GUI工具 项目地址: https://gitcode.com/gh_mirrors/nc/ncmppGui 还在为网易云音乐的NCM加密文件无法在其他播放器播放而烦恼吗&#x…

作者头像 李华
网站建设 2026/7/2 4:58:04

Ansible 遇见 AI:从自动化到智能化的运维新纪元(小白也能懂)

Ansible 遇见 AI:从自动化到智能化的运维新纪元(小白也能懂) 你是否曾想过,让服务器管理像和朋友聊天一样简单?当红炸子鸡的 AI 遇上老牌自动化工具 Ansible,究竟能碰撞出怎样的火花?这篇文章带…

作者头像 李华
网站建设 2026/7/2 4:57:28

loss.backward() 到底在干什么?一篇讲透计算图与反向传播

loss.backward() 到底在干什么?一篇讲透计算图与反向传播整理说明:本文基于 B 站视频【第05讲《计算图与反向传播:梯度如何流动》】公开信息、课程逐字稿与配套资料进行原创整理。不是逐字转写,而是把核心概念、手算流程、代码练习…

作者头像 李华
网站建设 2026/7/2 4:56:16

【从0到1构建一个ClaudeAgent】规划与协调-子Agent

在 原作者的Python代码 里,run_subagent 函数就像一个“虫洞”,把任务传送到一个新的平行宇宙(子线程/子上下文)去执行,执行完只带回结果。在 Java 中,我们通常通过创建新的类实例来实现这种隔离。父 Agent…

作者头像 李华
网站建设 2026/7/2 4:55:42

从算卦到具身:一套跨越三千年的“不确定系统建模”抽象

将东方古典象数哲学与现代前沿物理/人工智能架构进行深度互译。从算卦到具身:一套跨越三千年的“不确定系统建模”抽象 在过去的三千年里,人类文明一直在用两种截然不同的语言,追问同一个问题: 在一个充满随机性、非线性和熵增的宇…

作者头像 李华