Day 6:LangChain 入门——框架是双刃剑
摘要: Java工程师宸一分享AI Agent学习第6天心得:用LangChain框架重写手写Agent。通过对比手写(300行)与LangChain实现(100行),发现框架通过@tool装饰器自动生成工具定义,create_agent()封装复杂逻辑,大幅简化开发。核心收获: 框架优势:@tool自动注册工具并提取文档/参数,省去手动编写JSON Schema; 流程拆解:澄清tool_cal
🤖 系列:Java工程师转AI Agent 3个月学习计划
👤 作者:宸丶一 | 28岁Java程序员,规划狂魔,正在被AI Agent按头学习
🎯 今日目标: 用 LangChain 框架重写 Day 5 的 Agent,对比手写 vs 框架
💬 个人格言: 代码改不改变世界我不知道,但先让我准时下班。
前言
大家好,我是宸一,一个28岁的Java程序员。
今天是第6天,主题是:LangChain 入门。
昨天我们花了一整天手写了一个完整 Agent(Day 5),踩了 tool_call_id 的坑,理解了工具调用的底层流程。
今天换个思路:用 LangChain 框架重写同一个 Agent,看看框架帮我们省了多少事,又带来了什么代价。
有个意外收获:我们亲历了 LangChain 0.x → 1.3 的 Breaking Change,旧教程全部失效。这恰好验证了我们的学习路线——先手写理解原理,框架变了也不慌。
一、今日学习路线
核心对比:
Day 5 手写 Agent = OpenAI + TOOLS_DEFINITION + FullAgent 类(~300行)
LangChain Agent = ChatOpenAI + @tool + create_agent()(~100行)
二、LangChain 四大组件
2.1 用后端思维理解
| LangChain 概念 | Java 对应 | 作用 |
|---|---|---|
| ChatOpenAI | FeignClient | 调用大模型 API |
| @tool | @Component | 注册可调用的工具 |
| Chain | Pipeline | 把多个步骤串起来 |
| Agent | Controller | 决策:用哪个工具、怎么回答 |
2.2 @tool 装饰器做了什么?
# LangChain 方式(约 10 行/工具)
@tool
def get_weather(city: str) -> str:
"""获取指定城市的天气信息。
Args:
city: 城市名称,如"北京"、"上海"
"""
return f"{city}今天晴天,25°C"
@tool 做了三件事:
1. 注册 —— 告诉框架"这个函数是个可调用的工具"
2. 提取 —— 自动从函数名、docstring、类型注解生成工具描述
3. 包装 —— 把普通函数包装成框架能识别的 Tool 对象
对比 Day 5 手写(约 30 行/工具):
# Day 5 手写方式
TOOLS_DEFINITION = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"}
},
"required": ["city"]
}
}
}
]
三、用 LangChain 重写 Agent
3.1 核心代码
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
# 1. 定义工具
@tool
def get_weather(city: str) -> str:
"""获取指定城市的天气信息。"""
return f"{city}今天晴天,25°C"
@tool
def calculate(expression: str) -> str:
"""计算数学表达式。"""
return str(eval(expression))
# 2. 创建 Model
llm = ChatOpenAI(model="mimo-v2-flash", api_key=API_KEY, base_url=BASE_URL)
# 3. 创建 Agent(一行搞定)
agent = create_agent(
model=llm,
tools=[get_weather, calculate],
system_prompt="你是一个友好的AI助手。使用工具来回答问题。",
)
# 4. 调用 Agent(一行搞定)
result = agent.invoke({"messages": [HumanMessage(content="北京天气怎么样?")]})
对比 Day 5 的 FullAgent 类(~200行):
- Day 5:手写
_execute_tool()、_build_messages()、chat()等方法 - LangChain:
create_agent()一行创建,agent.invoke()一行调用
3.2 运行效果
📌 测试1:查天气
回答:北京今天晴天,温度25°C,适合出行!☀️
📌 测试2:算数学
回答:(15+27)*3 = 126
📌 测试3:查时间
回答:现在是2026年6月5日下午2点07分。
📌 测试4:搜索知识
回答:Python 是一种高级编程语言,特点是简洁易读。
四、重点:@tool 和 tool_call_id 的区别
4.1 我的理解偏差
随堂检测 Q2 问:@tool 怎么解决 tool_call_id 问题的?
我的错误回答:
“我感觉就是在 @tool 注册时给到了 tool_call_id,然后给到大模型,大模型就可以调用到了。”
这个理解是错的。 我把两个不同的环节混在一起了。
4.2 正确理解:三个阶段,三个主角
整个工具调用流程,分三个阶段:
阶段1:注册(你写代码时做的事) ← @tool 在这里
阶段2:决策(大模型做的事) ← tool_call_id 在这里生成
阶段3:执行(框架帮你做的事) ← tool_call_id 在这里传递
阶段1:注册 —— @tool 在这里起作用
时间点:程序启动时
主角:你 + @tool
目的:告诉大模型"我有哪些工具"
@tool 做了什么:
┌─────────────────────────────────────────────┐
│ 函数名 get_weather → 工具的 name │
│ docstring → 工具的 description │
│ 类型注解 city: str → 参数的 JSON schema │
└─────────────────────────────────────────────┘
这时候大模型知道了:我有一个叫 get_weather 的工具可以用。
但还没有人调用它。tool_call_id 还不存在。
阶段2:决策 —— 大模型在这里起作用
时间点:用户发消息"北京天气怎么样"
主角:大模型
目的:判断要不要调用工具
大模型返回:
tool_calls = [{
"id": "call_abc123", ← 这就是 tool_call_id!
"function": {
"name": "get_weather",
"arguments": {"city": "北京"}
}
}]
注意:
- id: "call_abc123" ← 大模型自动生成的唯一 ID
- 这个 ID 和 @tool 没有任何关系!
- @tool 是你注册工具时用的
- tool_call_id 是大模型决定调用工具时生成的
阶段3:执行 —— 框架在这里起作用
时间点:收到大模型的 tool_calls
主角:create_agent 背后的框架
目的:执行工具,把结果正确返回给大模型
框架做的三步:
第一步:找到工具
tool_calls[0].function.name = "get_weather"
→ 框架在 @tool 注册的工具列表里找到它
第二步:执行工具
get_weather(city="北京")
→ 得到结果:"北京今天晴天,25°C"
第三步:回传结果(关键!)
{
"role": "tool",
"tool_call_id": "call_abc123", ← 必须带上这个 ID
"content": "北京今天晴天,25°C"
}
4.3 为什么 tool_call_id 很重要?
因为大模型可能一次返回多个 tool_calls:
call_abc → get_weather("北京")
call_def → get_weather("上海")
call_ghi → calculate("1+1")
三个结果各自带着自己的 key 回去:
call_abc → "北京晴天 25°C"
call_def → "上海多云 28°C"
call_ghi → "2"
没有 tool_call_id,大模型分不清谁是谁。
就像 Java 里异步调用要关联 requestId 一样。
4.4 总结
阶段 主角 做什么 和 tool_call_id 的关系
─────────────────────────────────────────────────────────────────
注册 @tool 生成工具描述 无关
决策 大模型 决定调用哪个工具 它生成 tool_call_id
执行 框架 执行+回传结果 它传递 tool_call_id
Day 5 踩的坑:阶段3回传结果时漏了 tool_call_id → 报错
LangChain 帮你做的事:阶段3完全自动化,你不用管 tool_call_id
五、手写 vs 框架对比
5.1 代码量对比
+-------------------+-------------------------+-------------------------+
| 对比项 | Day 5 手写 | LangChain 框架 |
+-------------------+-------------------------+-------------------------+
| 工具定义 | 手写 JSON schema (30行) | @tool 装饰器 (10行) |
| 工具调用 | 手写 tool_call_id (50行) | 框架自动处理 (0行) |
| Agent 创建 | 自己写类 (200行) | create_agent() (1行) |
| 对话历史 | deque + JSON 文件 | Messages 列表 |
| 错误处理 | 自己实现重试 | 内置重试机制 |
| 代码量 | ~300 行 | ~100 行 |
+-------------------+-------------------------+-------------------------+
5.2 框架的代价
今天我们亲历了 LangChain 的 Breaking Change:
旧版(0.x):
from langchain.agents import create_tool_calling_agent, AgentExecutor
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools)
新版(1.3+):
from langchain.agents import create_agent
agent = create_agent(model=llm, tools=tools, system_prompt="...")
变化:
- AgentExecutor 没了 → 底层换成了 LangGraph
- input/chat_history → 统一用 messages 列表
- 更简洁,但旧教程全部失效
框架的三个代价:
1. 黑盒(相对的)
闭源产品 → 完全看不到代码,出问题只能等官方
开源框架 → 能看源码,但要花时间理解
自己写的代码 → 100% 透明
2. Breaking Change
版本更新可能不兼容,旧教程失效
3. 灵活性受限
自定义需求可能被框架限制
5.3 什么时候用框架,什么时候手写?
+-----------------------+-----------------------------------------------+
| 场景 | 建议 |
+-----------------------+-----------------------------------------------+
| 快速原型验证 | 用 LangChain(快速出活) |
| 学习原理 | 先手写再用框架(我们就是这么做的) |
| 生产环境 - 标准功能 | 用 LangChain(社区维护) |
| 生产环境 - 高度定制 | 手写核心逻辑(完全可控) |
| 生产环境 - 性能敏感 | 手写(减少框架开销) |
| 团队协作 | 用 LangChain(统一标准) |
| 个人项目 | 手写(更灵活) |
+-----------------------+-----------------------------------------------+
六、用后端思维总结
| LangChain 概念 | Java 对应 | 本例实现 |
|---|---|---|
| ChatOpenAI | FeignClient | 调用小米 MiMo API |
| @tool 装饰器 | @Component + 接口 | 注册 4 个工具函数 |
| ChatPromptTemplate | String.format | 定义系统提示 |
| create_agent | @Bean 工厂方法 | 一行创建 Agent |
| agent.invoke() | controller.method() | 调用 Agent 处理请求 |
| tool_call_id | requestId | 工具调用的唯一标识 |
七、今日收获
7.1 核心公式
LangChain Agent = ChatOpenAI + @tool + create_agent()
对比 Day 5:
Day 5 Agent = OpenAI + TOOLS_DEFINITION + FullAgent 类(300行)
LangChain Agent = ChatOpenAI + @tool + create_agent()(100行)
7.2 最大的收获
不是学会了 LangChain,而是理解了框架的本质。
框架 = 把重复的样板代码封装起来,让你专注于业务逻辑
但框架不是银弹:
- 版本更新可能 breaking change
- 灵活性可能受限
- 出问题可能不好调试
所以:
学习原理 → 先手写
提高效率 → 再用框架
生产选型 → 看场景
7.3 学习路线的价值
Day 1-5 手写 → Day 6 学框架
✅ 好处:
- 深入理解了 tool_call_id、消息格式等底层细节
- 知道框架帮你省了什么
- 框架变了也不慌,底层知识永远有用
❌ 代价:
- 花了更多时间
- 没按规划的时间节点完成
💡 结论:
"跑偏"不一定是坏事。
学习路线不是直线,而是螺旋上升。
八、明日计划(Day 7)
主题:部署入门 - FastAPI + Docker 基础
- 用 FastAPI 把 Agent 包装成 HTTP 服务
- 写 Dockerfile 容器化
- 测试 API 接口
- 思考生产环境还需要什么
一句话总结
框架是双刃剑:帮你省时间,但也可能坑你。先手写理解原理,再用框架提高效率。
Day 6 最大的收获不是学会了 LangChain,而是理解了 @tool 和 tool_call_id 是两个不同的环节——注册是注册,调用是调用,别混为一谈。
更多推荐


所有评论(0)