这是「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 轮:模型基于工具结果 -> 不再调工具 -> 给出最终答案三个关键观察:
- 模型没有凭记忆瞎编——它没有直接说"AAPL 大概 200 块",而是先调了工具。
- 循环自然停止——第 2 轮模型自己判断"数据够了",不再返回 tool_calls,循环结束。这个"模型自己决定何时停"就是 Agent 的自主性,也是它和写死步骤的普通脚本的本质区别。
- 整个 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 天走完。