🤖 系列: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,旧教程全部失效。这恰好验证了我们的学习路线——先手写理解原理,框架变了也不慌。


一、今日学习路线

01_langchain_basics.py
LangChain 基础

02_langchain_agent.py
用框架实现 Agent

03_comparison.py
手写 vs 框架对比

核心对比:

Day 5 手写 Agent = OpenAI + TOOLS_DEFINITION + FullAgent 类(~300行)
LangChain Agent  = ChatOpenAI + @tool + create_agent()(~100行)

二、LangChain 四大组件

2.1 用后端思维理解

1. Model
ChatOpenAI
= FeignClient

2. Tool
@tool 装饰器
= Strategy 策略模式

3. Chain
prompt | llm
= Pipeline 流水线

4. Agent
create_agent
= Controller 控制器

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 是两个不同的环节——注册是注册,调用是调用,别混为一谈。

Logo

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

更多推荐