LangChain Quick Start AI Agent 分析
上一篇 LangChain 1.2 入门学习 中使用了特定的 ChatOllama/ChatGoogleGenerativeAI, 或统一的 init_chat_model 方式创建一个与 LLM 的交互程序,并以此基础实现了一个简单的 AI Agent, 工具调用是根据响应中消息格式手工调用的, 会话处理是自己拼接全部对话历史。现在知名的 LLM 都支持推理,所以顺代也用灰色字体显示了 'thinking' 的文本内容。
本文直接学习官方 Quickstart 中的一个 AI Agent, 该 Agent
实现了以下功能
- 有了上下文,会记忆会话历史
- 支持调用多个工具
- 提供结构化一致性的响应数据格式
- 能通过上下文处理用户特定的信息
- 跨交互维护会话状态
写作本篇测试时用的是本地的 ollama:gemma4:26b 的模型
完整 AI Agent 代码
| |
本 Agent 中用 LangChain 的 create_agent() 方法创建一个 Agent, 并没有用流式来输出 LLM 过来的响应,而是用 agent.invoke()
进行了两次调用,两个问题分别是
- what is the weather outside?
- thank you so much!
执行 AI Agent
两个 print(response['structured_response']) 对应的输出分别为
ResponseFormat(pun_response="It's looking absolutely sun-sational in Florida! You might want to grab some shades, because the forecast is looking bright!", weather_conditions='Sunny')
ResponseFormat(pun_response="You're very welcome! I'm always happy to help brighten your day!", weather_conditions=None)
执行过程中连接到一个刚用 Vibe Coding 写的一个简单的 HTTP 代理程序,simple-http-proxy,
它可用来捕获与 Ollama API 的所有请求与响应。在 Python 程序端只要配置环境变量
1export http_proxy=http://127.0.0.1:9090
或者把 http_proxy=http://127.0.0.1:9090 放在 .env 文件中,由 dotenv.load_dotenv() 来加载也行。
执行后由代理记录下该 Agent 与 Ollama 的四个请求响应来回的数据,请点击 langchain-agent-ollama.txt
查看。后面的分析中会引用其中的内容,注意,响应数据中擦除了所有 'thinking' 的内容。
代码分析
我们对照代码与请求响应数据的内容对代码进行分析。
请求消息的组成
定义的 SYSTEM_PROMPT 在 create_agent() 时用 system_prompt 参数指定,它会在请求 messages 中作为第一条 role 为 system
的内容
1{
2 "messages": [
3 {
4 "role": "system",
5 "content": "You are an expert weather forecaster, who speaks in puns..."
6 }
7 ]
8}
与模型交互时,system_prompt, tools, 和 agent.invoke() 时的 input 才是传递给模型的内容,它们最终与请求的模型名称,模型参数组成
一个输入,我们看第一次 agent.invoke() 它会完成两次与 LLM 的交互, 分别实现 get_user_location 与 get_weather_for_location
工具的调用。第一次请求时完整的内容(这时格式化后的内容)
1{
2 "model": "gemma4:26b",
3 "stream": true,
4 "options": {
5 "temperature": 0.1
6 },
7 "messages": [
8 {
9 "role": "system",
10 "content": "You are an expert weather forecaster, who speaks in puns.\n\nYou have access to two tools:\n\n- get_weather_for_location: use this to get the weather for a specific location\n- get_user_location: use this to get the user's location\n\nIf a user asks you for the weather, make sure you know the location. If you can tell from the question\nthat they meanwherever they are, use the get_user_location tool to find their location."
11 },
12 {
13 "role": "user",
14 "content": "what is the weather outside?"
15 }
16 ],
17 "tools": [
18 {
19 "type": "function",
20 "function": {
21 "name": "get_user_location",
22 "description": "Retrieve user information based on user ID.",
23 "parameters": {
24 "type": "object",
25 "properties": {}
26 }
27 }
28 },
29 {
30 "type": "function",
31 "function": {
32 "name": "get_weather_for_location",
33 "description": "Get weather for a given city.",
34 "parameters": {
35 "type": "object",
36 "required": [
37 "city"
38 ],
39 "properties": {
40 "city": {
41 "type": "string"
42 }
43 }
44 }
45 }
46 },
47 {
48 "type": "function",
49 "function": {
50 "name": "ResponseFormat",
51 "description": "Response schema for the agent.",
52 "parameters": {
53 "type": "object",
54 "required": [
55 "pun_response"
56 ],
57 "properties": {
58 "pun_response": {
59 "type": "string"
60 },
61 "weather_conditions": {}
62 }
63 }
64 }
65 }
66 ]
67}
system_prompt 的 role 是 system, 用户的 role 是 user, 后面会看到 AI 响应内容的 role 是 assistant. 提示词中包含了工具
列表,ResponseFormat 也是以工具函数的方式存在,LLM 会找到它对应的格式参数,从而可用 response["structured_response"]
获得预期的格式输出。Context(user_id: str) 的信息只是存在于本地,为本地的工具函数所用, 从中获得 user_id 来管理上下文。
第一条响应数据
我们再来看请求 Ollama 的 /api/chat 后第一条消息的内容, 它是一个 Line-Delimited JSON, 即数据由一致多行组成,每一行是一个 JSON,
又称 NDJSON, 所以响应的 Content-Type 是 application/x-ndjson.
1{"model":"gemma4:26b","created_at":"2026-04-08T22:58:27.002360557Z","message":{"role":"assistant","content":"","tool_calls":[{"id":"call_detex1l0","function":{"index":0,"name":"get_user_location","arguments":{}}}]},"done":false}
2{"model":"gemma4:26b","created_at":"2026-04-08T22:58:27.009025523Z","message":{"role":"assistant","content":""},"done":true,"done_reason":"stop","total_duration":840090124,"load_duration":157000665,"prompt_eval_count":255,"prompt_eval_duration":18281146,"eval_count":99,"eval_duration":624183208}
这是两个 JSON, 第一行通知客户端调用工具 get_user_location. AI 回复的 role 是 assistant.
自动的会话记忆
看第二个请求的内容
1{
2 "model": "gemma4:26b",
3 "stream": true,
4 "options": {
5 "temperature": 0.1
6 },
7 "messages": [
8 {
9 "role": "system",
10 "content": "You are an expert weather forecaster, who speaks in puns.\n\nYou have access to two tools:\n\n- get_weather_for_location: use this to get the weather for a specific location\n- get_user_location: use this to get the user's location\n\nIf a user asks you for the weather, make sure you know the location. If you can tell from the question\nthat they meanwherever they are, use the get_user_location tool to find their location."
11 },
12 {
13 "role": "user",
14 "content": "what is the weather outside?"
15 },
16 {
17 "role": "assistant",
18 "tool_calls": [
19 {
20 "function": {
21 "name": "get_user_location",
22 "arguments": {}
23 }
24 }
25 ]
26 },
27 {
28 "role": "tool",
29 "content": "Florida"
30 }
31 ],
32 "tools": [ "<与上同,此处省略>" ]
33}
我们并没有拼接 message, 就因为我们在 create_agent() 是指定了 checkpointer=checkpointer, 即 InMemorySaver,
这种拼接会话的功能变成自动的了。上面的 messages, 在最初的
"role": "system", "content": "
的基础上,加上了第一次回复的内容
"role": "assistant", "tool_calls": [{"function": { "name": "get_user_location", "arguments": {}}}]
再加上新的工具调用的结果的内容
"role": "tool", "content": "Florida"
后面的请求也是由 InMemorySaver 持续的添加内容。
结构化响应数据
在调用完了工具 get_user_location 和 get_weather_for_location 后,我们得到了最终的回复,这时候的回复内容是
1{"model":"gemma4:26b","created_at":"2026-04-08T22:58:29.288326047Z","message":{"role":"assistant","content":"","tool_calls":[{"id":"call_maio2ggd","function":{"index":0,"name":"ResponseFormat","arguments":{"pun_response":"It's looking absolutely sun-sational in Florida! You might want to grab some shades, because the forecast is looking bright!","weather_conditions":"Sunny"}}}]},"done":false}
2{"model":"gemma4:26b","created_at":"2026-04-08T22:58:29.294894197Z","message":{"role":"assistant","content":""},"done":true,"done_reason":"stop","total_duration":1364325806,"load_duration":122615522,"prompt_eval_count":312,"prompt_eval_duration":22412919,"eval_count":184,"eval_duration":1153149215}
这时候要求我们再调用工具 ResponseFormat, 并且参数是 LLM 按照 ResponseFormat 的参数要求的内容
1{
2 "pun_response": "It's looking absolutely sun-sational in Florida! You might want to grab some shades, because the forecast is looking bright!",
3 "weather_conditions": "Sunny"
4}
Agent 端实际调用一下 ResponseFormat 工具函数就都到我们想要的结构化响应数据了,即 response['structured_response'] 的内容。
转换成 stream() 方式并与用户实时交互
官方 Quickstart 的代码是用 invoke() 非流式的交互,并且用户问题也是预设好的。
我们可以把它改成流式的交互方式,并且用户问题也通过 input() 来获取,这样就可以与用户实时交互了。启动 Claude Code, 只需要改动
1config: RunnableConfig = {"configurable": {"thread_id": "1"}}
后的代码为如下
1def stream_agent(messages: list[dict]) -> None:
2 for chunk in agent.stream(
3 {"messages": messages},
4 config=config,
5 context=Context(user_id="1"),
6 stream_mode="updates",
7 ):
8 for node, update in chunk.items():
9 print(f"--- [{node}] ---")
10 if "structured_response" in update:
11 print(update["structured_response"])
12 elif "messages" in update:
13 for msg in update["messages"]:
14 print(f" {type(msg).__name__}: {msg.content or msg.tool_calls}")
15
16while True:
17 user_input = input("You: ").strip()
18 if not user_input:
19 continue
20 stream_agent([{"role": "user", "content": user_input}])
执行时交互效果如下
1python src/langchain_study/agent3.py
2You: what is the weather outside?
3--- [model] ---
4 AIMessage: [{'name': 'get_user_location', 'args': {}, 'id': 'ca4dd0f4-904d-4603-9a75-6dbfe281e1b3', 'type': 'tool_call'}]
5--- [tools] ---
6 ToolMessage: Florida
7--- [model] ---
8 AIMessage: [{'name': 'get_weather_for_location', 'args': {'city': 'Florida'}, 'id': 'f479ecff-2444-4841-bc5d-a6269039d18e', 'type': 'tool_call'}]
9--- [tools] ---
10 ToolMessage: It's always sunny in Florida!
11--- [model] ---
12ResponseFormat(pun_response="It's looking bright and sunny in Florida! I'm feeling quite *bright* about this forecast!", weather_conditions='Sunny')
13You: thank you so much!
14Deserializing unregistered type __main__.ResponseFormat from checkpoint. This will be blocked in a future version. Add to allowed_msgpack_modules to silence: [('__main__', 'ResponseFormat')]
15--- [model] ---
16 AIMessage: You're *weather* welcome! Don't hesitate to reach out if you need another *forecast*!
17You:
继续丰富的话,可以自定义 / 命令,如 /clear 清理会话,/compact 压缩会话, /model 切换模型等。
而且从这里也看到与不同的 role 'tool', 'assistant' 对应的 ToolMessage, AIMessage. 而另外两个 role user 和 system
对应的类没列出,它们分别是 HumanMessage 和 SystemMessage. 它们的基类都是 BaseMessage, 除了前面四个实现类外,还有 RemoveMessage,
ChatMessage, 和 FunctionMessage.
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。