环境准备

ollama部署qwen2.5:7b参考:https://hua-ri.cn/2025/08/llm/ollama/ollama-bu-shu-qwen25-7b/

UV安装

mac环境安装命令:

1
curl -LsSf https://astral.sh/uv/install.sh | sh

安装python 3.13

1
2
3
4
5
# 查看已安装的python版本
uv python list

# 安装python版本3.13
uv python install 3.13

进入工作空间

1
mkdir -p llm && cd llm

创建工作目录:prompt

1
2
3
4
5
# 使用指定python版本初始化工作目录
uv init prompt -p 3.13

# 进入工作目录
cd prompt

安装依赖

1
2
3
4
5
6
7
# 添加依赖
uv add langchain langgraph langchain_openai langchain_core dotenv IPython


# 对于使用pip作为依赖关系的项目
pip install -U langchain langgraph

案例流程

在这个例子中,我们将创建一个帮助用户生成prompt的聊天机器人。它将首先收集用户的需求,然后生成prompt,并根据用户输入进行逐步优化。这两个状态被分为两个独立的状态,LLM 决定何时在它们之间转换。
该系统的图形表示如下所示。

img_2.png

这个例子里的机器人有两个主要“工作模式”或者说“状态”(state,也可以想象成节点,或者 agent):

  1. 收集信息状态 (Gather Information):先跟你聊天,问清楚你想要什么样的指令模板。比如,这个模板的目标是啥?里面要包含哪些变量?输出结果有啥限制或要求?
  2. 生成提示词状态 (Generate Prompt):信息收集够了,它就切换到这个状态,根据你给的信息,帮你写出那个指令模板。

LangGraph 这个库,就是用来帮你管理这种多状态、多步骤的复杂AI应用的“流程控制器”。

LLM配置

通过.envdotenv实现llm配置的传入。

.env

我是通过ollama本地部署qwen2.5:7b,这里可以根据实际情况自行变更。

1
2
3
4
5
LLM_API_KEY=sk-ollama
LLM_MODEL_NAME=qwen2.5:7b
LLM_BASE_URL=http://localhost:11434/v1
LLM_TEMPERATURE = 0
LLM_MAX_TOKENS = 512

可以如此家在配置:

1
2
from dotenv import load_dotenv
load_dotenv()

对应的通过ChatOpenAI初始化llm实例:

1
2
3
4
5
6
7
llm = ChatOpenAI(
model=os.getenv("LLM_MODEL_NAME"),
api_key=os.getenv("LLM_API_KEY"),
base_url=os.getenv("LLM_BASE_URL"),
temperature=os.getenv("LLM_TEMPERATURE"),
max_tokens=os.getenv("LLM_MAX_TOKENS")
)

收集信息

首先,让我们定义图谱中用于收集用户需求的部分。这将是一个带有特定系统消息的 LLM 调用。它将能够访问一个工具,当它准备好生成提示时可以调用该工具。

本笔记本使用 Pydantic v2 BaseModel,需要langchain-core >= 0.3。
langchain-core < 0.3由于混合使用 Pydantic v1 和 v2 ,使用 会导致错误BaseModels。

API 参考:SystemMessage | ChatOpenAI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from typing import List

from langchain_core.messages import SystemMessage
from langchain_openai import ChatOpenAI

from pydantic import BaseModel


template = """
你的任务是从用户那里获取他们想要创建哪种类型的提示词模板信息。

你应该从他们那里获取以下信息:

- 提示词的目标是什么
- 哪些变量会传递到提示词模板中
- 输出不应该做的任何限制条件
- 输出必须遵守的任何要求

如果你无法识别这些信息,请要求他们澄清!不要试图胡乱猜测。

在你能够识别所有信息后,调用相关的工具。"""


def get_messages_info(messages):
return [SystemMessage(content=template)] + messages


class PromptInstructions(BaseModel):
"""Instructions on how to prompt the LLM."""

objective: str
variables: List[str]
constraints: List[str]
requirements: List[str]


llm_with_tool = llm.bind_tools([PromptInstructions])


def info_chain(state):
messages = get_messages_info(state["messages"])
response = llm_with_tool.invoke(messages)
return {"messages": [response]}

生成提示

我们现在设置生成提示的状态。这将需要一个单独的系统消息,以及一个函数来过滤工具调用之前的所有消息(因为那是前一个状态决定生成提示的时间)。

(API参考:AIMessage |人类留言|工具消息)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage

# New system prompt
prompt_system = """Based on the following requirements, write a good prompt template:

{reqs}"""


# Function to get the messages for the prompt
# Will only get messages AFTER the tool call
def get_prompt_messages(messages: list):
tool_call = None
other_msgs = []
for m in messages:
if isinstance(m, AIMessage) and m.tool_calls:
tool_call = m.tool_calls[0]["args"]
elif isinstance(m, ToolMessage):
continue
elif tool_call is not None:
other_msgs.append(m)
return [SystemMessage(content=prompt_system.format(reqs=tool_call))] + other_msgs


def prompt_gen_chain(state):
messages = get_prompt_messages(state["messages"])
response = llm.invoke(messages)
return {"messages": [response]}

定义状态逻辑

这是聊天机器人所处状态的逻辑。如果最后一条消息是工具调用,那么我们处于“提示创建者”(prompt)应该响应的状态。否则,如果最后一条消息不是 HumanMessage,那么我们知道接下来应该由人工响应,因此我们处于此END状态。如果最后一条消息是 HumanMessage,并且之前有过工具调用,那么我们处于此prompt状态。否则,我们处于“信息收集”(info)状态。
(API 参考:END)

1
2
3
4
5
6
7
8
9
10
11
12
from typing import Literal

from langgraph.graph import END


def get_state(state):
messages = state["messages"]
if isinstance(messages[-1], AIMessage) and messages[-1].tool_calls:
return "add_tool_message"
elif not isinstance(messages[-1], HumanMessage):
return END
return "info"

创建图表

现在我们可以创建图表了。我们将使用 SqliteSaver 来保存对话历史记录。
(API 参考:InMemorySaver | StateGraph | START | add_messages)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START
from langgraph.graph.message import add_messages
from typing import Annotated
from typing_extensions import TypedDict


class State(TypedDict):
messages: Annotated[list, add_messages]


memory = InMemorySaver()
workflow = StateGraph(State)
workflow.add_node("info", info_chain)
workflow.add_node("prompt", prompt_gen_chain)


@workflow.add_node
def add_tool_message(state: State):
return {
"messages": [
ToolMessage(
content="Prompt generated!",
tool_call_id=state["messages"][-1].tool_calls[0]["id"],
)
]
}


workflow.add_conditional_edges("info", get_state, ["add_tool_message", "info", END])
workflow.add_edge("add_tool_message", "prompt")
workflow.add_edge("prompt", END)
workflow.add_edge(START, "info")
graph = workflow.compile(checkpointer=memory)

查看图

1
2
3
4
5
6
7
8
# 画图
png_bytes = graph.get_graph().draw_mermaid_png()

with open("graph.png", "wb") as f:
f.write(png_bytes)

import os
os.system("open graph.png")

img_3.png

使用图表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import uuid
cached_human_responses = ["哈喽!", "rag prompt", "1 rag, 2 none, 3 no, 4 no", "q"]
cached_response_index = 0
config = {"configurable": {"thread_id": str(uuid.uuid4())}}
while True:
user = cached_human_responses[cached_response_index]
cached_response_index += 1
print(f"User (q/Q to quit): {user}")
if user in {"q", "Q"}:
print("AI: Byebye")
break
output = None
for output in graph.stream(
{"messages": [HumanMessage(content=user)]}, config=config, stream_mode="updates"
):
last_message = next(iter(output.values()))["messages"][-1]
last_message.pretty_print()

if output and "prompt" in output:
print("Done!")

最后的代码

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
from typing import List
import os
import uuid
from langchain_core.messages import SystemMessage
from langchain_openai import ChatOpenAI

from pydantic import BaseModel
from dotenv import load_dotenv
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START
from langgraph.graph.message import add_messages
from typing import Annotated
from typing_extensions import TypedDict

from langgraph.graph import END


load_dotenv()


llm = ChatOpenAI(
model=os.getenv("LLM_MODEL_NAME"),
api_key=os.getenv("LLM_API_KEY"),
base_url=os.getenv("LLM_BASE_URL"),
temperature=os.getenv("LLM_TEMPERATURE"),
max_tokens=os.getenv("LLM_MAX_TOKENS")
)

template = """
你的任务是从用户那里获取他们想要创建哪种类型的提示词模板信息。

你应该从他们那里获取以下信息:

- 提示词的目标是什么
- 哪些变量会传递到提示词模板中
- 输出不应该做的任何限制条件
- 输出必须遵守的任何要求

如果你无法识别这些信息,请要求他们澄清!不要试图胡乱猜测。

在你能够识别所有信息后,调用相关的工具。"""


def get_messages_info(messages):
return [SystemMessage(content=template)] + messages


class PromptInstructions(BaseModel):
"""Instructions on how to prompt the LLM."""

objective: str
variables: List[str]
constraints: List[str]
requirements: List[str]


llm_with_tool = llm.bind_tools([PromptInstructions])


def info_chain(state):
messages = get_messages_info(state["messages"])
response = llm_with_tool.invoke(messages)
return {"messages": [response]}


# New system prompt
prompt_system = """Based on the following requirements, write a good prompt template:

{reqs}"""


# Function to get the messages for the prompt
# Will only get messages AFTER the tool call
def get_prompt_messages(messages: list):
tool_call = None
other_msgs = []
for m in messages:
if isinstance(m, AIMessage) and m.tool_calls:
tool_call = m.tool_calls[0]["args"]
elif isinstance(m, ToolMessage):
continue
elif tool_call is not None:
other_msgs.append(m)
return [SystemMessage(content=prompt_system.format(reqs=tool_call))] + other_msgs


def prompt_gen_chain(state):
messages = get_prompt_messages(state["messages"])
response = llm.invoke(messages)
return {"messages": [response]}


def get_state(state):
messages = state["messages"]
if isinstance(messages[-1], AIMessage) and messages[-1].tool_calls:
return "add_tool_message"
elif not isinstance(messages[-1], HumanMessage):
return END
return "info"


class State(TypedDict):
messages: Annotated[list, add_messages]


memory = InMemorySaver()
workflow = StateGraph(State)
workflow.add_node("info", info_chain)
workflow.add_node("prompt", prompt_gen_chain)


@workflow.add_node
def add_tool_message(state: State):
return {
"messages": [
ToolMessage(
content="Prompt generated!",
tool_call_id=state["messages"][-1].tool_calls[0]["id"],
)
]
}


workflow.add_conditional_edges("info", get_state, ["add_tool_message", "info", END])
workflow.add_edge("add_tool_message", "prompt")
workflow.add_edge("prompt", END)
workflow.add_edge(START, "info")
graph = workflow.compile(checkpointer=memory)

# 画图
png_bytes = graph.get_graph().draw_mermaid_png()

with open("graph.png", "wb") as f:
f.write(png_bytes)

import os
os.system("open graph.png")


cached_human_responses = ["哈喽!", "rag prompt", "1 rag, 2 none, 3 no, 4 no", "q"]
cached_response_index = 0
config = {"configurable": {"thread_id": str(uuid.uuid4())}}
while True:
user = cached_human_responses[cached_response_index]
cached_response_index += 1
print(f"User (q/Q to quit): {user}")
if user in {"q", "Q"}:
print("AI: Byebye")
break
output = None
for output in graph.stream(
{"messages": [HumanMessage(content=user)]}, config=config, stream_mode="updates"
):
last_message = next(iter(output.values()))["messages"][-1]
last_message.pretty_print()

if output and "prompt" in output:
print("Done!")

对话记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
User (q/Q to quit): 哈喽!
================================== Ai Message ==================================

你好!很高兴帮助你创建提示词模板。为了更好地理解你的需求,请告诉我:

1. 这个提示词的目标是什么?
2. 有哪些变量会传递到提示词模板中?
3. 输出不应该做的任何限制条件是什么?
4. 输出必须遵守的任何要求又是什么呢?

请尽量详细地提供这些信息,这样我可以更准确地帮助你。
User (q/Q to quit): rag prompt
================================== Ai Message ==================================

好的,让我们来明确一下“RAG”(Retrieval-Augmented Generation)提示词的目标和相关信息。

1. **目标**:RAG 提示词的主要目的是结合检索到的信息和生成的文本来增强最终输出的质量。
2. **变量**:
- `query`:用户的问题或查询。
- `context`:从数据库或其他来源检索的相关信息片段。
3. **限制条件**:输出不应包含任何未经过验证的事实,确保所有引用的信息都是可靠的。
4. **要求**:输出应清晰、简洁,并且在可能的情况下提供具体的例子来支持结论。

请确认这些信息是否准确,或者你是否有其他特定的要求或变量需要添加。
User (q/Q to quit): 1 rag, 2 none, 3 no, 4 no
================================== Ai Message ==================================

明白了!根据你的反馈,我们将创建一个简单的 RAG 提示词模板,不包含额外的限制条件和要求。

以下是提示词的目标、变量以及相关信息:

- **目标**:结合检索到的信息和生成的内容来增强最终输出的质量。
- **变量**:
- `query`:用户的问题或查询。
- `context`:从数据库或其他来源检索的相关信息片段。

现在,我将调用相关的工具来创建这个提示词模板。请稍等片刻。
Tool Calls:
PromptInstructions (call_jasy7crr)
Call ID: call_jasy7crr
Args:
constraints: []
objective: 结合检索到的信息和生成的内容来增强最终输出的质量
requirements: []
variables: ['query', 'context']
================================= Tool Message =================================

Prompt generated!
================================== Ai Message ==================================

```json
{
"promptTemplate": {
"intro": "根据以下提供的查询和上下文信息,您需要综合运用检索到的相关信息以及您的创造力来生成高质量的输出。请确保最终内容既包含相关背景知识又具有创新性。",
"variables": [
{
"name": "query",
"description": "用户的具体需求或问题描述,例如:关于如何在家制作美味蛋糕的方法和技巧。",
"example": "如何在家制作美味蛋糕"
},
{
"name": "context",
"description": "与查询相关的背景信息或限制条件,例如:需要考虑食材的可获得性、适合儿童操作等。",
"example": "使用家庭厨房常见的材料,确保步骤简单易懂且安全无毒"
}
],
"instructions": [
{
"step": 1,
"description": "首先,根据提供的查询和上下文信息进行初步分析,理解用户的具体需求。",
"example": "理解用户希望了解如何在家制作美味蛋糕,并考虑到使用家庭厨房常见的材料且步骤简单易懂且安全无毒。"
},
{
"step": 2,
"description": "利用检索到的相关信息来丰富和深化您的回答,确保内容的准确性和完整性。",
"example": "查找并整合关于制作蛋糕的基本知识、常见食材及其替代品、简单的食谱以及注意事项等。"
},
{
"step": 3,
"description": "结合检索到的信息和生成的内容来增强最终输出的质量,确保信息的连贯性和创新性。",
"example": "编写一份详细且易于理解的蛋糕制作指南,包括材料清单、步骤说明以及一些创意装饰建议。"
}
],
"outro": "完成上述步骤后,请检查您的回答是否满足所有要求,并确保内容既全面又具有吸引力。"
}
}
```

此模板旨在指导用户如何结合检索到的信息和生成的内容来增强最终输出的质量,同时提供了具体的变量说明、操作步骤以及示例以帮助理解。
Done!
User (q/Q to quit): q
AI: Byebye