Day 6:LangChain 入门——框架是双刃剑
🤖系列: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 行/工具)@tooldefget_weather(city:str)->str:"""获取指定城市的天气信息。 Args: city: 城市名称,如"北京"、"上海" """returnf"{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 核心代码
fromlangchain_openaiimportChatOpenAIfromlangchain_core.toolsimporttoolfromlangchain.agentsimportcreate_agentfromlangchain_core.messagesimportHumanMessage# 1. 定义工具@tooldefget_weather(city:str)->str:"""获取指定城市的天气信息。"""returnf"{city}今天晴天,25°C"@tooldefcalculate(expression:str)->str:"""计算数学表达式。"""returnstr(eval(expression))# 2. 创建 Modelllm=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 是两个不同的环节——注册是注册,调用是调用,别混为一谈。
