《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 轮:模型基于工具结果 -> 不再调工具 -> 给出最终答案

三个关键观察:

  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 天走完。

源码链接: https://pan.quark.cn/s/a4b39357ea24 DMA(直接内存访问)是计算机系统中一种关键的数据传输机制,它使得特定的硬件子系统得以直接对系统内存进行读写操作,无需CPU的介入。这种机制对于提高I/O操作的效能具有极其重要的作用,特别是在网络设备、存储设备等驱动程序的编写过程中占据着核心地位。Cache(缓存)则是一种用于暂存频繁访问的数据和指令的存储结构,其目的是减少处理器对主存储器的访问次数,进而增强系统的整体性能。然而,DMA和Cache之间存在着一致性的挑战,特别是在部分嵌入式系统中,DMA操作可能绕过Cache机制,从而引发数据一致的情况,这就需要采取一系列策略来维护Cache的一致性。 在DMA的运作模式中,主要存在两种Cache一致性问题:流式DMA(streaming DMA)与一致性DMA(coherent DMA)。流式DMA通常应用于需要大量数据传输的场景,它关注Cache的一致性,因此传输速度较快,但要求软件开发者自行管理数据的一致性。而一致性DMA则保证了在DMA传输期间,数据在Cache与主内存之间保持同步,通常适用于对一致性要求较高的应用场景。 在Linux内核中,为了有效管理DMA操作,提供了一系列接口函数。其中,一致性DMA接口负责维护数据的一致性,而流式DMA接口则提供了更快的传输速度,但要求开发者自行解决数据一致性的问题。开发者在选用这些接口时,必须依据硬件平台的特点和性能需求,选择合适的DMA模式。 Cache一致性的解决方案通常取决于硬件平台的属性。在某些先进的处理器架构中,Cache对程序员而言是透明的,即处理器与Cache控制器之间的交互对程序员可见,从而简化了编程的复...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值