上一篇我说了从Java转大模型,我选了Python不选Java。有人问:Python调个API谁不会,有什么好写的?
确实,调API谁都会。但当你想做个真正能用的大模型应用------多轮对话、结构化输出、工具调用------就得用框架了。Python生态里,LangChain就是事实标准。
于是我开始学LangChain。3天踩了5个坑,每个坑都让我怀疑自己是不是选错了语言------但最后发现,LangChain的设计确实比Java那套优雅得多,只是需要先过思维这关。
先说结论
Java人学LangChain,最大的敌人不是Python语法,是思维惯性。
Java训练你"精确控制"------写Service、定义DTO、做异常处理。LangChain说:别写了,声明一下就行。
5个坑踩完,你会爱上这种写法。
坑1:以为要写一堆类,结果一个Chain搞定
我的第一反应
搞一个AI应用,Java后端的本能反应是这样的:
java
// Java思维:先定义接口
public interface ChatService {
ChatResponse chat(String message);
}
// 再写实现
@Service
public class ChatServiceImpl implements ChatService {
private final PromptBuilder promptBuilder;
private final LlmClient llmClient;
private final OutputParser outputParser;
@Override
public ChatResponse chat(String message) {
String prompt = promptBuilder.build(message);
String raw = llmClient.call(prompt);
return outputParser.parse(raw);
}
}
接口、实现、注入、三层架构,写Java的人觉得这才是正经代码。
LangChain的现实
ini
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# 三行代码,一个链
prompt = ChatPromptTemplate.from_template("你是一个客服,回答用户问题:{question}")
model = ChatOpenAI(model="qwen-plus")
chain = prompt | model | StrOutputParser()
# 直接调用
response = chain.invoke({"question": "退货流程是什么?"})
print(response)
管道符 | 串起来,Prompt → Model → Parser,一行一个环节。
我第一次看到这个写法的反应:这......不用写实现类?不用try-catch?一行管道就搞定了?
没错。这就是LangChain的核心思想------声明式组合。你不需要写"怎么做",只需要说"按什么顺序做"。
如果用Java的思路来类比:
arduino
LangChain的Chain ≈ Spring的Bean装配链
prompt | model | parser ≈ @Bean的依赖注入顺序
chain.invoke() ≈ 调用链入口方法
但说实话,这个类比反而限制了你的理解。LangChain的管道比Spring的Bean链灵活得多------你可以随时插入、替换、分支任何一个环节,不需要改接口、不需要重新编译。这就是Python的动态性带来的好处,Java做不到这么轻。
坑2:Prompt模板拼接,被花括号坑了
我的第一反应
ini
String prompt = "你是客服,回答:" + userInput; // 拼字符串,简单粗暴
Java里字符串拼接太常见了,我想LangChain里也能这么干。
翻车现场
ini
# ❌ 错误示范
template = "请帮我翻译以下内容为英文:" + user_input
prompt = ChatPromptTemplate.from_template(template)
结果用户输入里带了 {name},LangChain直接报错:
vbnet
KeyError: 'name'
因为LangChain的模板用 {变量名} 做占位符,拼进去的字符串里有花括号,它以为是模板变量,找不到就炸了。
正确做法
ini
# ✅ 用模板变量,不要拼字符串
template = "请帮我将以下内容翻译为英文:\n{content}"
prompt = ChatPromptTemplate.from_template(template)
chain = prompt | model | StrOutputParser()
response = chain.invoke({"content": user_input}) # 变量通过字典传入
如果用熟悉的Java概念来理解:
javascript
ChatPromptTemplate ≈ MessageFormat.format()
{变量名} ≈ {0}、{1}占位符
invoke({"content": "xxx"}) ≈ format(new Object[]{"xxx"})
核心原则:永远不要拼字符串,永远用模板变量。 和Java里不用拼SQL用PreparedStatement一个道理------防注入。大模型应用同样有注入风险,这个坑不踩的话,上线就是安全漏洞。
坑3:以为Memory自动清理,结果Token爆了
我的第一反应
c
// 聊天记录存Redis,设置TTL自动过期,稳得很
redisTemplate.opsForValue().set("chat:" + sessionId, messages, 30, TimeUnit.MINUTES);
在Java里,缓存有TTL、有淘汰策略、有最大容量,基本上不会出事。
翻车现场
加了对话记忆,跑了两轮对话,Token直接超限报错:
python
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# 我以为Memory会自动管理长度......
memory = ChatMessageHistory()
它不会。 ChatMessageHistory 把所有历史消息原封不动塞进请求,聊10轮=10轮的上下文全发过去,Token当然爆。
这就像一个没有TTL、没有容量上限的Redis------用Java的话说就是内存泄漏。
正确做法
ini
from langchain_core.messages import SystemMessage, trim_messages
# 用trim_messages控制上下文长度
trimmed = trim_messages(
messages,
max_tokens=2000, # 最多2000个Token
strategy="last", # 保留最近的
token_counter=model, # 用模型的Token计数器
include_system=True, # 始终保留系统消息
)
或者在会话层面手动裁剪:
python
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# 自己管理:只保留最近5轮
def get_session_history(session_id: str) -> InMemoryChatMessageHistory:
history = store.get(session_id)
if history is None:
history = InMemoryChatMessageHistory()
store[session_id] = history
# 手动裁剪:只保留最近10条消息
if len(history.messages) > 10:
history.messages = history.messages[-10:]
return history
大模型的上下文窗口有硬上限,Memory不等于无限存储,必须自己裁剪。 这一点Java人反而容易理解------不就是LRU淘汰策略吗?道理一样,只是LangChain没帮你做,得自己动手。
坑4:模型返回的不是标准JSON,解析崩了
我的第一反应
kotlin
// 后端同学的习惯:定义DTO,Jackson反序列化,稳
@Data
public class ProductDTO {
private String name;
private Double price;
}
ProductDTO dto = objectMapper.readValue(json, ProductDTO.class);
定义好DTO,接口返回的JSON只要格式对,Jackson直接反序列化,不会有任何意外。
翻车现场
我想让模型返回结构化数据,用了 JsonOutputParser:
python
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel
class Product(BaseModel):
name: str
price: float
parser = JsonOutputParser(pydantic_object=Product)
看起来很完美,对吧?然后模型返回了:
json
这是您要的产品信息:
{
"name": "MacBook Pro",
"price": "一万四"
}
两个问题:
- 前面带了废话("这是您要的...")
- price给的是中文"一万四",不是数字
解析直接炸了:OutputParserException: Failed to parse
模型不是API! API你定义好DTO它就乖乖按格式返回,模型是"人"------它会加废话,会用中文写数字,会忘了加引号。和后端对接前端不同,你不能假设对方是程序员。
正确做法
python
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(description="产品名称")
price: float = Field(description="价格,纯数字,单位元")
parser = JsonOutputParser(pydantic_object=Product)
# 关键:把格式要求写进Prompt
prompt = ChatPromptTemplate.from_template(
"""请根据用户需求提取产品信息。
{format_instructions}
用户需求:{question}""",
partial_variables={"format_instructions": parser.get_format_instructions()}
)
chain = prompt | model | parser
parser.get_format_instructions() 会自动生成一段格式说明塞进Prompt里,告诉模型:"只返回JSON,不要废话,price必须是数字"。
用Java的概念来理解:
scss
Pydantic ≈ Lombok + Hibernate Validator
Field(description=) ≈ @ApiModelProperty(给模型看的字段说明)
get_format_instructions() ≈ 自动生成Swagger示例文档给模型看
但注意:Pydantic比Lombok强太多了。 它不仅能验证数据,还能自动生成"告诉模型怎么返回"的指令。Java里你得手写Swagger文档+手动校验,Python里一行Field(description=)全搞定------这也是我选Python不选Java的原因之一。
坑5:工具描述不清晰,模型调错工具
我的第一反应
typescript
// 写接口,方法名+参数类型+注释,清清楚楚
/**
* 查询订单状态
* @param orderId 订单ID
*/
public OrderStatus queryOrder(String orderId) { ... }
在Java里,方法签名就是最好的文档------方法名、参数类型、返回类型,一看就懂。
翻车现场
给Agent配了两个工具:查订单和查物流。结果用户问"我的订单到哪了",模型调了查订单而不是查物流。
为什么?因为我的工具描述写的是:
python
@tool
def query_order(order_id: str) -> str:
"""查询订单"""
...
就"查询订单"四个字,模型理解成"查订单状态"也行,"查物流"也行------它选错了。
这里有个关键区别:Java的方法是给程序员看的,LangChain的工具描述是给AI看的。 程序员能从方法签名推断语义,AI只能从你的文字描述来理解------写模糊了,它就猜。
正确做法
python
@tool
def query_order(order_id: str) -> str:
"""根据订单号查询订单的支付状态和金额。注意:不包含物流信息,查物流请用query_logistics。"""
...
@tool
def query_logistics(order_id: str) -> str:
"""根据订单号查询物流轨迹和当前配送状态。当用户问"到哪了""什么时候到""快递到哪了"时使用此工具。"""
...
三个技巧:
- 说清楚这个工具做什么,不说"查询",说"查询支付状态和金额"
- 说清楚这个工具不做什么,"不包含物流信息"
- 列出触发场景,"当用户问'到哪了'时使用"
less
@tool描述 ≈ Swagger的@ApiOperation + @ApiParam
模型选工具 ≈ 根据方法注释判断调哪个Service
描述不清晰 ≈ 接口文档写得太模糊,前端调错接口
工具描述就是你写给AI的"接口文档"。 你在Java里写接口文档有多认真,在LangChain里写工具描述就得有多认真。
完整代码:一个能跑的智能客服
5个坑都避开后的完整版本:
python
"""
用LangChain搭的智能客服
依赖:pip install langchain langchain-openai
"""
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# 1. 初始化模型(用阿里的通义千问,国内直连)
model = ChatOpenAI(
model="qwen-plus",
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
api_key="你的API Key", # 建议放环境变量
)
# 2. Prompt模板(不拼字符串!用模板变量!)
prompt = ChatPromptTemplate.from_template(
"""你是荣码商城的客服,友好、专业、简洁。
当前对话:
{history}
用户:{question}"""
)
# 3. 基础链
chain = prompt | model | StrOutputParser()
# 4. 加对话记忆(手动裁剪,防止Token爆炸)
store = {}
def get_session_history(session_id: str):
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
history = store[session_id]
# 只保留最近6条消息
if len(history.messages) > 6:
history.messages = history.messages[-6:]
return history
chain_with_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="question",
history_messages_key="history",
)
# 5. 运行
if __name__ == "__main__":
print("=== 荣码商城AI客服(输入q退出)===")
while True:
question = input("\n你:")
if question.lower() == "q":
break
response = chain_with_history.invoke(
{"question": question},
config={"configurable": {"session_id": "user-001"}},
)
print(f"客服:{response}")
运行效果:
arduino
=== 荣码商城AI客服(输入q退出)===
你:退货流程是什么?
客服:您好!退货流程如下:1. 在订单详情页点击"申请退货"...(省略)
你:需要多久能退款?
客服:一般3-5个工作日原路退回。您刚才问的退货流程,提交后可以在订单页查看进度哦。
注意第二句回复------它记住了上一轮的上下文。这就是Memory的作用。
5个坑的思维转换总结
| # | Java思维 | LangChain思维 | 本质区别 |
|---|---|---|---|
| 1 | 写实现类 | 声明Chain | 从"怎么做"到"按什么顺序做" |
| 2 | 拼字符串 | 用模板变量 | 防注入 + 结构化 |
| 3 | Redis存Session | Memory + 手动裁剪 | 上下文有窗口上限 |
| 4 | 定义DTO | Pydantic + Prompt格式指令 | 模型不是API,得告诉它格式 |
| 5 | 接口文档给前端 | 工具描述给模型 | 描述越精确,选择越准确 |
一句话总结:Java训练你"精确控制",LangChain要求你"精确描述"。
你不再写"怎么做",而是写"要什么"。这个思维转变,是学LangChain最大的坎------但迈过去之后,你会发现写代码快了3倍。
为什么我踩完坑更坚定选Python了
这5个坑里,坑1和坑4最能说明问题:
坑1------Chain的管道式组合。 prompt | model | parser,随时插入、替换、分支任何一个环节。Java要实现同样的灵活性?你得写接口+工厂+策略模式+配置文件,50行变5行的事,Java得写200行。
坑4------Pydantic自动生成格式指令。 一个 Field(description=) 搞定了Java里DTO+Swagger文档+校验三件事。Python的动态性在这里不是缺点,是实实在在的生产力。
LangChain4j和Spring AI也在追赶,但说实话------同样是做Chain,Python版3行代码能跑,Java版你得先配好Spring Boot项目、引入依赖、写配置类。学到后面差距只会更大。
转就转彻底,别回头。
你学LangChain踩过什么坑?或者还在纠结要不要从Java转Python?评论区聊聊 👇
👉 下一篇:RAG实战------用Python让AI读懂公司文档,50行代码搞定
点赞关注「荣码」,大模型转型系列持续更新~