Day 5:从零搭建一个完整 Agent——动手才是硬道理
🤖 系列: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 一开始我直接给了完整代码让学生跑,结果发现"跑通了≠理解了"。所以后半段我们加了思考题,从"会用"升级到"懂原理"。
下面把今天的核心内容整理出来,有代码、有踩坑、有思考。
一、今日学习路线
核心公式:
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 工具调用流程
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 三层记忆架构
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_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 如何防御提示词注入?
我的回答: 用向量数据库建立"危险区"。
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_calls时content为None
先手写再用框架,理解更深刻。
7.3 规划 vs 实际
按照原计划,Week 2 应该学 LangChain。但我们选择了先手写理解原理:
好处:
- ✅ 深入理解了 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世界。
声明: 本文为原创学习笔记,如需转载请注明出处。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)