
本节完成官方案例: Quickstart->Build a reeal-world agent , 地址:https://docs.langchain.com/oss/python/langchain/quickstart#build-a-real-world-agent, 同时完善相关概念
1.案例中相关概念梳理
这个案例总共提到了6个步骤,其中涉及到了一些概念,这里先对这些概念做一些简短的解释。
1.1 提示词:Prompt
在 LangChain 中,Prompt(提示词) 的演进经历了从"简单的字符串"到"结构化消息序列"的过程。
LangChain 将输入抽象为 Messages。每条消息都包含两个核心要素:角色 (Role) 和 内容 (Content)。
| 消息类型 (Class) | 对应角色 (Role) | **核心用途 ****** |
|---|---|---|
SystemMessage |
System | 设置 AI 的"人格"、背景规则或任务约束。 |
HumanMessage |
User | 用户输入的指令或问题。 |
AIMessage |
Assistant | 模型生成的响应 |
如果我们正在构建一个法律咨询机器人,希望它始终以"资深律师"的语气回答,并且不允许讨论食谱。那么像这种 "身份设定"和"禁令"应该放在**SystemMessage **消息中 ,它正是存放这类"全局规则"和"身份设定"的地方。 它就像是给 AI 植入的"出厂设置",确保它在后续对话中始终维持律师的专业性,并遵守禁令。
那么我们应该怎样来组织这些消息?
在 LangChain 1.0 中,我们通常会将这些消息组合成一个列表 (List)。一个典型的法律对话模板看起来是这样的:
- SystemMessage: "你是一位资深律师,只回答法律相关问题,严禁讨论食谱。"
- HumanMessage: "你好,我想咨询关于合同纠纷的问题..."
- AIMessage: "您好,我是您的法律助手。关于合同纠纷,请问具体涉及到哪方面的条款?"
通过这种结构化的消息序列,模型能更清晰地分辨哪些是指令 ,哪些是对话历史。
1.2 外部工具(@tool)
在 LangChain 中,LLM 本身无法知道模型本身以外的知识,它只能"预测文本"。Tools 就是给 AI 安装的"插件"或"手臂"。
@tool 装饰器**:这是最简单的创建方式。当你给一个 Python 函数加上这个装饰器时,LangChain 会自动解析函数的名称 、参数类型和**文档字符串(Docstring)。
AI 并不是直接运行代码,而是通过阅读你的 Docstring 来判断"我什么时候该用这个工具"。所以,在写
@tool时,文档字符串就是写给 AI 看的"使用说明书"。
1.3 运行时上下文 (Runtime Context)
你可以把它理解为职员工作时的"工牌信息"或"环境配置"。 当你在处理多用户请求时,通过 Runtime Context,你可以让 Agent 知道当前是在为哪个用户服务,而不需要把这些琐碎的元数据写进 Prompt 里。
1.4 Agent 记忆 (Short-term Memory)
在前面的章节中,已经了解过记忆管理了。 普通的对话模型只记得"你说过什么",但 Agent 的记忆更复杂:
- 它需要记住对话历史(Human 和 AI 的对话)。
- 它还需要记住思考过程(Intermediate Steps):比如它刚才调用了搜索工具,搜到了什么结果。
1.5 结构化输出 (@dataclass)
当我们要求 LLM "以 JSON 格式输出并包含姓名和年龄"时,LangChain 会利用这些定义好的数据类,把 LLM 吐出来的字符串自动转换成 Python 对象。 @dataclass 提供了明确的字段名和类型提示。
2.案例代码实现
这里逐步实现案例的每个步骤,帮助理解它是如何运行起来的。会对原案例进行适当的改写。
2.1 定义系统提示词
这里将原案例中的提示词改写为中文:
python
if __name__ == '__main__':
SYSTEM_PROMPT = """你是一位专业的天气预报员,说话时还喜欢用双关语。
你可以使用下面两个工具:
- get_weather_for_location: 使用它来获取指定地点的天气情况
- get_user_location: 使用它来获取用户的地理位置
如果用户向您询问天气情况,请务必先确认其所在位置。如果从问题中可以推断出他们指的是其所在的具体地点,那么请使用"get_user_location"工具来查找他们的位置。
"""
2.2 创建工具(@tool)
系统提示词中提到了两个工具:get_weather_for_location和 get_user_location.这两个函数上使用了Python 装饰器 @tool.
2.2.1 运行时上下文参数
在这些工具具体的实现里面,如果希望获取到一些有用的信息,比如:用户ID(用户业务系统中的ID,或者其他信息)该如何获取?这就要用到运行时上下文(Runtime Context) ,这个运行时上下文中具体包含什么样的数据类型,我们需要定义一个具体的类型,如下代码:
python
@dataclass
class Context:
"""Custom runtime context schema."""
user_id: str
@dataclass 并不是 LangChain 定义的,它是 Python 标准库 (从 3.7 版本开始引入)自带的一个装饰器 , 它的核心作用是自动为你生成样板代码。
通常在 Python 中,如果你想创建一个简单的类来存储数据,你需要手动写很多方法。对比一下:
不使用 **@dataclass**:
python
class Context:
def __init__(self, user_id: str):
self.user_id = user_id
def __repr__(self):
return f"Context(user_id='{self.user_id}')"
使用 @dataclass:
python
from dataclasses import dataclass
@dataclass
class Context:
user_id: str
当你加上这个装饰器时,Python 会自动帮你生成 __init__(构造函数)、__repr__(用于打印对象的字符串表示)以及 __eq__(用于比较两个对象是否相等)等方法。
上面例子中:Context 被用来定义 Runtime Context 的结构。使用 @dataclass 有几个好处:
- 结构清晰 :一眼就能看出这个上下文里包含哪些字段(如
user_id)及其类型。 - 类型安全 :LangChain 的工具(Tools)可以通过类型检查知道
runtime.context到底是什么数据结构。 - 易于解析:由于它是结构化的,LangChain 内部可以方便地将配置参数(如字典)映射到这个对象上。
2.2.2 定义工具
有了运行时上下文参数,我们在定义工具的时候就可以使用它了。
python
from langchain.tools import tool, ToolRuntime
@tool
def get_weather_for_location(city: str) -> str:
"""获取给定城市(city)的天气"""
return f"{city} 的天气是晴朗的, 温度是25摄氏度!"
@tool
def get_user_location(runtime: ToolRuntime[Context]) -> str:
"""获取用户所在城市."""
user_id = runtime.context.user_id
return "北京" if user_id == "1" else "上海"
在 get_weather_for_location 中,虽然没有在注释中写 city: 北京,但 LLM 是通过函数签名 和 Docstring 的组合来理解:
- 函数名 :
get_weather_for_location暗示了需要一个位置。 - 参数名 :
city是一个非常有语义的单词。 - Docstring :
"获取给定城市(city)的天气."明确告诉了 LLM 这个函数的作用。
当 LangChain 将这个函数转换成 JSON Schema 发送给 LLM 时,它会告诉模型:"我有一个工具,它需要一个名为 city 的字符串。" LLM 会根据用户的提问(比如"上海天气如何?")自动提取出"上海"并填入这个 city 参数。
第二个工具,get_user_location``ToolRuntime[Context] 是如何传递进来的?
这个参数并不是由 LLM 提供的,而是由 LangChain 框架在运行时"注入" (Injection) 的。当 LangChain 把工具描述发给 LLM 时,它会识别出 ToolRuntime 这个类型,并自动从发给 LLM 的 Schema 中剔除它。所以 LLM 根本不知道有这个参数,也就不会尝试去填它。
当调用 agent.invoke(..., config={"configurable": {"user_id": "1"}}) 时,框架会将这些配置包装进 Context 对象,并在执行函数前,自动把当前的运行环境填充到 runtime 参数里。
LangChain 在执行工具前会进行 类型检查 (Type Inspection) 。如果发现参数类型是
ToolRuntime或其相关子类,它就会去当前的RunnableConfig中寻找匹配的数据填充进去,而剩下的参数则去匹配 LLM 返回的 JSON。
2.3 配置模型
python
from langchain.chat_models import init_chat_model
model=init_chat_model(
model="ollama:deepseek-v3.1:671b-cloud",
base_url="http://127.0.0.1:11434")
2.4 定义响应结构化数据
我们希望agent响应数据的时候,自动将模型响应的数据构建成我们自己定义的数据结构,现在只需要自定义一个类接口,需要注意的是编写注释中语义要明确
python
@dataclass
class ResponseFormat:
"""agent响应数据的 schema."""
# 有趣,调皮风格的回应 (必须)
punny_response: str
# 常规的回应
weather_conditions: str | None = None
2.5 添加记忆管理
这里使用内存记忆,这个在上一章已经介绍过了。
python
from langgraph.checkpoint.memory import InMemorySaver
checkpointer = InMemorySaver()
2.6 完整可运行代码
python
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool, ToolRuntime
from langgraph.checkpoint.memory import InMemorySaver
@dataclass
class Context:
"""
自定义运行时上下文参数数据结构
"""
user_id: str
@tool
def get_weather_for_location(city: str) -> str:
'''
获取给定城市(city)的天气
:param city: 城市
:return:
'''
return f"{city} 的天气是晴朗的, 温度是25摄氏度!"
@tool
def get_user_location(runtime: ToolRuntime[Context]) -> str:
"""获取用户所在城市."""
user_id = runtime.context.user_id
return "北京" if user_id == "1" else "上海"
@dataclass
class ResponseFormat:
"""Response schema for the agent."""
# A punny response (always required)
punny_response: str
# Any interesting information about the weather if available
weather_conditions: str | None = None
if __name__ == '__main__':
# 1. 定义系统提示词
SYSTEM_PROMPT = """
你是一位专业的天气预报员,说话时还喜欢用双关语。
你可以使用下面两个工具:
- get_weather_for_location: 使用它来获取指定地点的天气情况
- get_user_location: 使用它来获取用户的地理位置
如果用户向您询问天气情况,请务必先确认其所在位置。如果从问题中可以推断出他们指的是其所在的具体地点,那么请使用"get_user_location"工具来查找他们的位置。
"""
# 2. 创建工具:上面的 get_weather_for_location 和 get_user_location 函数
# 3. 配置模型
model = init_chat_model(
model="ollama:qwen3-next:80b-cloud",
base_url="http://127.0.0.1:11434")
# 4.定义响应结构化数据 上面的 ResponseFormat
# 5. 记忆管理
checkpointer = InMemorySaver()
# 6. 创建 Agent
agent = create_agent(
model=model, # 模型
system_prompt=SYSTEM_PROMPT, # SystemMessage
tools=[get_user_location, get_weather_for_location], # 工具
context_schema=Context, # 运行时上下文参数数据结构Context是自定义的类
response_format=ResponseFormat, # 响应结构化数据
checkpointer=checkpointer # 记忆管理
)
# 7. 会话,运行时上下文参数
config = {"configurable": {"thread_id": "123456"}}
runtime_context = Context(user_id="1000")
# 8. HumanMessage,第一次提问
prompt_first = [{"role": "user", "content": "今天天气如何?"}]
# 9. 运行
response = agent.invoke(
{"messages": prompt_first},
config=config,
context=runtime_context
)
# 10. 输出结构化响应
# print(response)
for message in response["messages"]:
message.pretty_print()
print("*" * 80)
# 11. HumanMessage,第二次提问
prompt_second = [{"role": "user", "content": "谢谢!"}]
response = agent.invoke(
{"messages": prompt_second},
config=config,
context=runtime_context
)
# 12. 输出结构化响应
# print(response)
for message in response["messages"]:
message.pretty_print()
输出:
latex
=============== Human Message ==============
今天天气如何?
=============== Ai Message ==============
Tool Calls:
get_user_location (0b540a6d-8c75-4bf9-8ea5-cfba99c81e6b)
Call ID: 0b540a6d-8c75-4bf9-8ea5-cfba99c81e6b
Args:
=============== Tool Message ==============
Name: get_user_location
上海
=============== Ai Message ==============
Tool Calls:
get_weather_for_location (5897eba2-5ec1-4e7d-ba82-6b2603ebec17)
Call ID: 5897eba2-5ec1-4e7d-ba82-6b2603ebec17
Args:
city: 上海
=============== Tool Message ==============
Name: get_weather_for_location
上海 的天气是晴朗的, 温度是25摄氏度!
=============== Ai Message ==============
{
"punny_response": "上海今天晴空万里,25度,真是'晴'人好天气,记得'晒'出好心情!",
"weather_conditions": {
"condition": "晴朗",
"temperature": "25摄氏度"
}
}
********************************************************************************
=============== Human Message ==============
今天天气如何?
=============== Ai Message ==============
Tool Calls:
get_user_location (0b540a6d-8c75-4bf9-8ea5-cfba99c81e6b)
Call ID: 0b540a6d-8c75-4bf9-8ea5-cfba99c81e6b
Args:
=============== Tool Message ==============
Name: get_user_location
上海
=============== Ai Message ==============
Tool Calls:
get_weather_for_location (5897eba2-5ec1-4e7d-ba82-6b2603ebec17)
Call ID: 5897eba2-5ec1-4e7d-ba82-6b2603ebec17
Args:
city: 上海
=============== Tool Message ==============
Name: get_weather_for_location
上海 的天气是晴朗的, 温度是25摄氏度!
=============== Ai Message ==============
{
"punny_response": "上海今天晴空万里,25度,真是'晴'人好天气,记得'晒'出好心情!",
"weather_conditions": {
"condition": "晴朗",
"temperature": "25摄氏度"
}
}
=============== Human Message =================================
谢谢!
=============== Ai Message ==================================
不客气!上海今天晴空万里,25度,祝你"晴"心如意,"暖"意融融!☀️
可以看到结果输出的时候,被格式化成了JSON格式。注意如果发现结果没有被正确的结构化输出,有可能是模型并不支持结构化输出。比如我将上面的模型更换成
deepseek-v3.1:671b-cloud就发现它无法进行结构化输出。