《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
"""
import json
from openai import OpenAI
client = OpenAI(
api_key="你的key",
base_url="你的endpoint", # 用官方就删掉这行
)
MODEL = "gpt-4o-mini" # 或你 endpoint 支持的模型名
# ====== 工具实现(假数据,专注看循环) ======
def get_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()
if key in fake_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 主循环:
def run_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},
]
for step in range(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:模型不再调工具 -> 给出最终答案,退出循环
if not msg.tool_calls:
print("[final answer produced]")
return msg.content or ""
# 情况 B:模型要调工具 -> 执行 + 把结果喂回去
messages.append(msg.model_dump(exclude_unset=True))
for call in msg.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) if impl else {"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 天走完。
更多推荐

所有评论(0)