🤖 系列:Java工程师转AI Agent 3个月学习计划
👤 作者:宸丶一 | 28岁Java程序员,规划狂魔,正在被AI Agent按头学习
🎯 今日目标: 从零搭建一个能对话、能调工具、能记住用户的完整Agent
💬 个人格言: 代码改不改变世界我不知道,但先让我准时下班。


前言

大家好,我是宸一,一个28岁的Java程序员。

今天是第5天,主题是:动手实现迷你 Agent

前四天我们一直在"纸上谈兵":

  • Day 1:大模型API基础
  • Day 2:工具调用与Function Calling
  • Day 3:记忆系统与向量数据库
  • Day 4:Agent核心架构理论

今天终于要把理论变成代码了!

这次的学习方式依然是"1对1问答式"——但有个变化:先跑代码,再问为什么。

说实话,Day 5 一开始我直接给了完整代码让学生跑,结果发现"跑通了≠理解了"。所以后半段我们加了思考题,从"会用"升级到"懂原理"。

下面把今天的核心内容整理出来,有代码、有踩坑、有思考。


一、今日学习路线

01_simple_agent.py
最简 Agent

02_agent_with_tools.py
带工具的 Agent

03_agent_with_memory.py
带记忆的 Agent

04_full_agent.py
完整版 Agent

核心公式:

Agent = 系统提示 + 对话历史 + 工具调用 + 记忆系统 + 错误处理

二、第一步:最简 Agent

2.1 核心代码

from openai import OpenAI
from collections import deque

class SimpleAgent:
    def __init__(self):
        self.client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
        self.model = "mimo-v2-flash"
        self.history = deque(maxlen=10)  # 滑动窗口,最多10轮

        # 系统提示 = 全局配置
        self.history.append({
            "role": "system",
            "content": "你是一个友好的AI助手,名叫小助手。"
        })

    def chat(self, user_input: str) -> str:
        # 1. 记录用户输入
        self.history.append({"role": "user", "content": user_input})

        # 2. 调用大模型
        response = self.client.chat.completions.create(
            model=self.model,
            messages=list(self.history)
        )

        # 3. 提取回复
        reply = response.choices[0].message.content

        # 4. 记录回复
        self.history.append({"role": "assistant", "content": reply})
        return reply

2.2 运行效果

🧑 你: 你好
🤖 Agent: 你好呀!👋 我是小助手,很高兴认识你~ 😊

🧑 你: 你是谁?
🤖 Agent: 我是小助手,一个友好的AI助手,就像你的数字小伙伴一样!🤖

🧑 你: history
📜 对话历史:
  [0] 🔧 System: 你是一个友好的AI助手,名叫小助手。
  [1] 🧑 User: 你好
  [2] 🤖 Agent: 你好呀!👋 我是小助手,很高兴认识你~ 😊

2.3 用后端思维理解

Agent 概念 Java 对应
self.client = OpenAI() new RestTemplate()
self.history = deque(maxlen=10) Deque<Message> history
SYSTEM_PROMPT application.yml 配置

三、第二步:带工具的 Agent

3.1 工具定义(Function Calling 协议)

TOOLS_DEFINITION = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的天气信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名称"}
                },
                "required": ["city"]
            }
        }
    }
]

# 工具函数映射(等价于 Java 的 Map<String, Tool>)
TOOL_FUNCTIONS = {
    "get_weather": get_weather,
    "calculate": calculate,
}

3.2 工具调用流程

👤 用户:北京天气怎么样?

🤖 大模型:需要调用 get_weather

🔧 执行:get_weather(city=北京)

📊 返回:北京晴天 25°C

🤖 大模型:组织语言

💬 回答:北京今天晴天,25°C,适合出行 ☀️

3.3 运行效果

🧑 你: 北京天气怎么样?

🔧 调用工具:get_weather
   参数:{'city': '北京'}
   结果:北京今天晴天,温度 25°C,适合出行 ☀️

🤖 Agent: 北京今天晴天,温度 25°C,适合出行 ☀️

🧑 你: 帮我算一下 (10+5)*2

🔧 调用工具:calculate
   参数:{'expression': '(10+5)*2'}
   结果:(10+5)*2 = 30

🤖 Agent: (10+5)*2 = 30

3.4 关键理解:谁在决定调不调工具?

是大模型在决策,Agent 代码只是执行者。

大模型:"我判断需要调用 get_weather"
Agent 代码:"好的,我帮你调"
大模型:"结果是这个,我来组织语言"

四、第三步:带记忆的 Agent

4.1 三层记忆架构

🧠 记忆系统

重要信息提取

📝 短期记忆
deque(maxlen=20)
等价于 Redis 缓存

💾 长期记忆
JSON 文件
等价于 MySQL 数据库

4.2 记忆提取

class MemoryExtractor:
    PATTERNS = {
        "name": ["我叫", "我是", "我的名字"],
        "hobby": ["我喜欢", "我的爱好"],
    }

    @classmethod
    def extract(cls, text: str) -> dict:
        results = {}
        for category, keywords in cls.PATTERNS.items():
            for keyword in keywords:
                if keyword in text:
                    parts = text.split(keyword)
                    if len(parts) > 1:
                        results[category] = parts[1].strip()
                    break
        return results

4.3 运行效果

🧑 你: 我叫宸一
💾 长期记忆已保存:name = 宸一
🤖 Agent: 你好呀,宸一!很高兴认识你!😊

🧑 你: 我喜欢Python
💾 长期记忆已保存:hobby = Python
🤖 Agent: 太棒了,宸一!我也很喜欢 Python!🐍

🧑 你: 你还记得我吗?
🤖 Agent: 当然记得呀,宸一!😄 我记得你喜欢 Python!

--- 重启程序 ---

🧑 你: 你还记得我吗?
🤖 Agent: 当然记得!你是宸一,喜欢 Python!

五、第四步:完整版 Agent(踩坑记录)

5.1 关键踩坑:tool_call_id

问题: 工具调用时报错 tool_call_id is not set

✅ 正确做法

保留完整的
tool_calls 结构

工具结果必须带
tool_call_id

正常运行 ✅

❌ 错误做法

存储消息时
丢失 tool_call_id

大模型无法关联
工具调用和结果

报错 💥

原因: 存储消息时丢失了 tool_call_id

# ❌ 错误做法:把消息存成纯文本
self.memory.add_to_short_term("assistant", "[调用工具]")
self.memory.add_to_short_term("tool", tool_result)

# ✅ 正确做法:保留完整的消息结构
self.memory.add_message({
    "role": "assistant",
    "content": None,
    "tool_calls": [{
        "id": tc.id,                    # ← 必须保留 id
        "type": "function",
        "function": {
            "name": tc.function.name,
            "arguments": tc.function.arguments
        }
    }]
})

self.memory.add_message({
    "role": "tool",
    "tool_call_id": tool_call.id,       # ← 关键:必须带这个 id
    "content": tool_result
})

5.2 完整运行效果

🧑 你: 我叫宸一
💾 记住了:name = 宸一
🤖 Agent: 你好宸一!很高兴认识你。有什么我可以帮你的吗?

🧑 你: 北京天气怎么样?
🔧 调用工具:get_weather({'city': '北京'})
   ✅ 结果:北京今天晴天,温度 25°C,适合出行 ☀️
🤖 Agent: 北京今天晴天,温度 25°C,适合出行 ☀️

🧑 你: 你还记得我吗?
🤖 Agent: 记得呀,宸一!我们之前聊过天气呢。

六、进阶思考题(问答式学习的精华)

跑通代码后,我被问了几个有深度的问题。这些问题让我从"会用"升级到"懂原理"。

6.1 工具调用为什么需要二次调用大模型?

我的回答: 因为工具调用是中间状态,不是最终结果。

用户问 → 大模型思考(需要工具)→ 执行工具 → 大模型总结 → 用户

类比后端:

Controller → Service 判断需要查 DB → 查 DB → Service 组装结果 → 返回前端

6.2 当前记忆提取有什么局限?

我的回答: 关键词匹配太死板。

# 当前实现
if "我叫" in text:
    name = text.split("我叫")[1]

# 问题:"我是宸一,搞Java的" 没有"我叫",提取失败

更好的方案: 让大模型帮你提取

prompt = "从这句话中提取用户信息:'我是宸一,搞Java的'"
# 大模型返回:{name: "宸一", job: "Java开发"}

6.3 eval 注入如何防御?

我的回答: 不要用 eval,用安全的替代方案。

# ❌ 危险
eval("__import__('os').system('rm -rf /')")

# ✅ 方案1:ast.literal_eval(只能解析字面量)
import ast
ast.literal_eval("2+3*4")

# ✅ 方案2:正则校验
import re
if re.match(r'^[\d+\-*/(). ]+$', expression):
    result = eval(expression)

6.4 如何防御提示词注入?

我的回答: 用向量数据库建立"危险区"。

> 0.8

< 0.5

👤 用户输入

🔢 向量化

🔍 和危险库比较
相似度

🚫 拦截!

✅ 放行

dangerous_patterns = ["忽略之前的指令", "你现在是一个黑客", ...]
danger_db.add(dangerous_patterns)

def is_dangerous(user_input):
    results = danger_db.query(user_input, n_results=1)
    return results[0]["distance"] < 0.3  # 相似度太高 = 危险

比关键词匹配强:能识别语义相近的变体。


七、今日收获

7.1 核心概念对照表

Agent 概念 Java 后端对应 本例实现
System Prompt application.yml SYSTEM_PROMPT 常量
对话历史 Deque deque(maxlen=20)
工具注册表 Map<String, Tool> TOOL_FUNCTIONS 字典
工具调用 策略模式 Function Calling
tool_call_id 请求关联ID 必须传递的标识
短期记忆 Redis 缓存 deque
长期记忆 MySQL 数据库 JSON 文件
错误处理 try-catch + 重试 retry_with_backoff
提示词注入防御 SQL注入防御 向量数据库危险区

7.2 踩坑的价值

tool_call_id 这个坑踩得值。

如果直接用 LangChain 框架,这些细节都被封装了,你永远不会知道:

  • 工具调用需要 tool_call_id 关联
  • 消息格式必须严格遵循协议
  • 大模型返回 tool_callscontentNone

先手写再用框架,理解更深刻。

7.3 规划 vs 实际

按照原计划,Week 2 应该学 LangChain。但我们选择了先手写理解原理

调整

🎯 实际执行

Day 2:手写 Function Calling

Day 5:手写完整 Agent

Day 6:学 LangChain

📅 原规划

Week 2:学 LangChain

Week 4:学部署

好处:

  • ✅ 深入理解了 tool_call_id、消息格式等底层细节
  • ✅ 知道框架帮你省了什么
  • ✅ 踩坑记忆深刻

代价:

  • ❌ 花了更多时间
  • ❌ 没有按时间节点完成

结论: "跑偏"不一定是坏事。学习路线不是直线,而是螺旋上升。


八、写在最后

Day 5 是一个转折点。

前四天我们一直在"学概念",今天终于"写代码"了。

说实话,一开始我觉得"跑通代码"就够了,但被追问了几个问题后才发现——跑通≠理解

比如 tool_call_id 这个东西,如果直接用 LangChain,你永远不会知道它的存在。但手写一遍,踩一次坑,就再也忘不了了。

这就是"先手写再用框架"的价值。

明天我们要学 LangChain 了,有了手写的基础,相信会更有感觉。


📌 系列目录

  • Day 1:环境搭建与大模型API基础
  • Day 2:LangChain核心与工具调用
  • Day 3:记忆系统与向量数据库
  • Day 4:Agent核心架构全解析
  • Day 5:动手实现完整Agent(本文)
  • Day 6:LangChain入门(即将更新)

标签: #AI Agent #Java工程师 #Agent开发 #工具调用 #记忆系统 #踩坑记录 #学习笔记


关于作者: 宸丶一,28岁Java程序员,规划狂魔,正在用AI学AI。

💬 格言: “代码改不改变世界我不知道,但先让我准时下班。”

🎯 目标: 3个月转AI Agent,用后端思维拆解AI世界。

声明: 本文为原创学习笔记,如需转载请注明出处。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐