前言
在上一篇文章里,我们只做了一件事:
在自己的电脑上,把大模型跑起来,并让它回一句话。
如果你能顺利跑完上一篇,其实已经超过了很多"只停留在概念层"的人。
这一篇,我们继续往前走一步。
不讲复杂原理,不讲论文,也暂时不做真正的 RAG 系统。
这一篇只解决一个问题:怎么让模型"按我说的规则"来回答,而不是自由发挥。
一、在继续之前,先把几个角色说清楚
在正式写代码前,我们先把几个名词一次性说清楚。
不然你后面一定会懵,而且不是你的问题。
1. Ollama 是干什么的?
在上一篇里,我们用过:
arduino
ollama run gemma:1b
它做的事情只有一件:
在你本地启动一个大模型,并提供一个服务,让你能跟它对话。
你可以把 Ollama 理解为:
- 本地大模型的"运行环境"
- 或者更直白一点:发动机
但注意:
Ollama 只负责"模型能跑", 不负责"你怎么用得好"。
2. 那 LangChain 是什么?
LangChain 不是模型,也不是替代 Ollama 的东西,它是帮助我们开发大模型应用的框架
一句话版本:
LangChain 是一个帮你"用代码稳定、可控地使用大模型"的工具库。
如果继续用刚才的比喻:
-
Ollama 是发动机
-
LangChain 更像是:
- 方向盘
- 刹车
- 仪表盘
你当然可以"直接踩油门", 但一旦你想:
- 控制输入结构
- 约束输出格式
- 管理上下文
- 后面做 RAG / Agent
你几乎一定会用到 LangChain 这一层。
3. ChatOllama 和 ollama run 有什么区别?
这是小白最容易卡住的一点。
-
ollama run gemma:1b- 是给"人"用的
- 在命令行里聊天
-
ChatOllama- 是给"程序"用的
- 让 Python 代码去调用本地模型
结构关系可以简单理解成:
你的 Python 程序
↓
LangChain(ChatOllama)
↓
Ollama 服务
↓
本地大模型(gemma)
所以这一篇,并不是推翻上一篇,而是:
从"人跟模型聊天",升级为"程序在使用模型"。
二、第一步:让模型知道「你是谁,它该怎么回答」
代码地址:github.com/wxwwt/ai-kn...
里面存放了我写的练习的例子,下面的代码都在examples这个目录下面
我们先看第一个练习文件(ex01_hello_llm.py)。
ini
from langchain_ollama import ChatOllama
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
def main():
# 1. 创建模型(本地 Ollama)
llm = ChatOllama(
model="gemma3:1b",
temperature=0.7,
)
# 2. 构造消息
messages = [
SystemMessage(content="你是一个严谨、简洁的技术助手"),
HumanMessage(content="什么是 RAG?"),
AIMessage(content="RAG 是一种将检索与生成结合的技术。"),
HumanMessage(content="那它解决了什么问题?"),
]
# 3. 调用模型
response = llm.invoke(messages)
# 4. 输出结果
print(response.content)
if __name__ == "__main__":
main()
我们一步步拆解,这里第一次正式引入了 LangChain 的消息结构。
javascript
from langchain_ollama import ChatOllama
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
这三个 Message,看起来只是"不同名字", 但它们背后代表的,是三种完全不同的角色。
1. System / Human / AI 分别是什么?
可以这样理解:
-
SystemMessage:
- 规则说明书
- 身份设定
- 行为边界
-
HumanMessage:
- 用户提出的问题
-
AIMessage:
- 模型之前的回答(历史上下文)
非常重要的一点是:
你不是在给模型"发一条消息", 而是在构造一整段"对话历史"。
模型会综合这整段内容来决定怎么回答,其实对于大模型本身来说,上面三种不同的角色实际上都是prompt的一部分,没有什么本质的区别,但是对于开发AI应用的人来说就非常有用,因为它从工程化的角度上区分开了到底是什么角色在输出信息,容易调试和修改。
2. 一个最简单、但非常关键的例子
ini
messages = [
SystemMessage(content="你是一个严谨、简洁的技术助手"),
HumanMessage(content="什么是 RAG?"),
AIMessage(content="RAG 是一种将检索与生成结合的技术。"),
HumanMessage(content="那它解决了什么问题?"),
]
这里做了两件很重要的事:
- 你明确告诉模型:它的角色是什么
- 你把"上一轮对话"显式塞给了模型
如果你后面做过多轮问答、RAG、Agent会反复用到这种模式,就像我们平时自己使用大模型产品,也不是只会输出一轮对话,而是会产生很多轮的对话,还有历史的对话信息。
三、第二步:让模型按格式回答
模型会回答问题,并不等于"能被程序使用"。
真实开发里,我们通常希望:
- 输出是结构化的
- 能被代码解析
- 不掺杂多余废话
第二个练习文件,就是在解决这件事。
ini
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_ollama import ChatOllama
def main():
# 1. 创建模型(本地 Ollama)
llm = ChatOllama(
model="gemma3:1b",
temperature=0.7,
)
system_message = """
你是一个遵循 ReAct 格式的助手。
回答时必须严格按照以下格式:
Thought: 你的内部思考(简短)
Answer: 给用户的最终回答
"""
user_message = "为什么 LLM 需要 system / user / assistant 这三种角色?"
# 2. 构造消息
messages = [
SystemMessage(content=system_message),
HumanMessage(content=user_message),
]
# 3. 调用模型
response = llm.invoke(messages)
# 4. 输出结果
_, answer = parse_response(response.content)
print(answer)
def parse_response(text: str):
thought_lines = []
answer_lines = []
current = None
for line in text.splitlines():
if line.startswith("Thought:"):
current = "thought"
thought_lines.append(line.replace("Thought:", "").strip())
elif line.startswith("Answer:"):
current = "answer"
answer_lines.append(line.replace("Answer:", "").strip())
else:
if current == "thought":
thought_lines.append(line)
elif current == "answer":
answer_lines.append(line)
thought = "\n".join(thought_lines).strip()
answer = "\n".join(answer_lines).strip()
print(f"Thought: {thought}")
print(f"Answer: {answer}")
return thought, answer
if __name__ == "__main__":
main()
1. 什么是 ReAct?这里为什么要用它?
先说清楚一件事:
我们这里用 ReAct,不是为了论文,也不是为了复杂的自动化推理。
在这一篇里,它只有一个作用:
作为一种"输出格式约定"。
在 system message 里明确告诉模型:
makefile
回答时必须严格按照以下格式:
Thought: ...
Answer: ...
模型只要遵守这个格式,我们的程序就能稳定地把结果拆出来用。
2. 为什么要这么"折腾"?
因为从这一刻开始:
模型不只是"给人看的",而是"给程序用的"。
这里写的 parse_response,本质上就是在做一件事:
- 把自然语言
- 转换成程序可控的数据结构
而且有没有发现,除了我们接受到大模型的调用,实际上也需要工程上的事情要做,想着这种字符串处理,格式准换之类的,AI应用开发就是这种介于算法和工程之间的复合型工作。
四、第三步:第一次真正限制模型的"知识来源"
前面两步,模型其实依然是自由的。
它可以使用训练数据里的常识, 也可以自行推断。
第三个练习文件,开始触碰 RAG 的核心思想。
ini
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_ollama import ChatOllama
def main():
# 1. 创建模型(本地 Ollama)
llm = ChatOllama(
model="gemma3:1b",
temperature=0.7,
)
# system_message = """
# 你只能根据提供的上下文回答。
# 如果上下文中没有相关信息,必须原样输出:
# Thought: 我不知道
# Answer: 我不知道
# 禁止使用常识、猜测、历史经验、训练数据。
# """
# user_message = "什么是project 23协议"
KNOWLEDGE_BASE = """
【内部项目记录】
项目代号:Project Aurora-42
该项目于 2025-12-15 内部启动,目标是为一家
中型制造企业构建私有化 AI 知识库系统。
关键技术选型:
- 后端语言:Python 3.12
- LLM 运行方式:Ollama 本地部署
- 向量数据库:Chroma
- 框架:LangChain
特殊约定:
Aurora-42 项目中,"蓝鲸协议"指的是一种
内部定义的数据同步流程,与公开互联网无关。
"""
system_message = f"""
System:
你只能根据以下 Context 回答问题。
Context:
{KNOWLEDGE_BASE}
你是一个遵循 ReAct 格式的助手。
回答时必须严格按照以下格式:
Thought: 你的内部思考(简短)
Answer: 给用户的最终回答
"""
user_message = "aurora-42是在哪一天启动的?"
# 2. 构造消息
messages = [
# SystemMessage(content=system_message),
HumanMessage(content=user_message),
]
# 3. 调用模型
response = llm.invoke(messages)
# 4. 输出结果
_, answer = parse_response(response.content)
print(answer)
def parse_response(text: str):
thought_lines = []
answer_lines = []
current = None
for line in text.splitlines():
if line.startswith("Thought:"):
current = "thought"
thought_lines.append(line.replace("Thought:", "").strip())
elif line.startswith("Answer:"):
current = "answer"
answer_lines.append(line.replace("Answer:", "").strip())
else:
if current == "thought":
thought_lines.append(line)
elif current == "answer":
answer_lines.append(line)
thought = "\n".join(thought_lines).strip()
answer = "\n".join(answer_lines).strip()
print(f"Thought: {thought}")
print(f"Answer: {answer}")
return thought, answer
if __name__ == "__main__":
main()
1. 手写一个"最小知识库"
我们直接在代码里写了一段文本,这个知识是我们临时编的,一定是不存在llm的原有知识里面的,所以它一定回答不出来或者会胡编乱造:
yaml
【内部项目记录】
项目代号:Project Aurora-42
该项目于 2025-12-15 内部启动
...
这一步非常重要,因为它在训练你一个关键意识:
模型不一定要"什么都知道", 它也可以只知道你给它的东西。
但是你发现我们问它,这个项目是什么时候启动的,它是可以回答出来的,因为这个外部知识被我们传入到prompt里面去了。这个就是rag的核心思想,只不过不是像我们这么简陋,直接把答案放到prompt里面去了。真正生产级别的rag是会用到向量数据库的,将用户的问题转换为向量,和数据库里面的向量进行相似度的计算,然后找出来最相似的几个文字片段,在一起交给llm总结,分析后输出。
虽然听起来很简单,实际上要做的工程上的东西是不少的,比如用户输入的问题里面有很多无用的信息,直接根据向量搜索很有可能匹配到非常多无用的信息,另外如果相似度返回了几百个文件,那么哪一些文件是应该放在最前面的,也就是分数最高的?这种规则怎么处理之类的?其实里面有非常多的问题要处理。
2. System Message 的真正作用
在 system message 里写的规则,其实非常"狠":
- 只能基于 Context 回答
- 上下文没有,就说不知道
- 禁止常识、猜测和训练数据
因为我们本地这个小模型智商其实挺低的,你问它不知道的东西,它会瞎编,如果不加这个约束,它就给你说一堆有的没的,所以我们为了了解rag的基本原理就要加上这个限制。当然你可以用参数更大的模型比如8b的,或者api调用大模型产商的也也可以。但是如果你没有显卡,或者显卡显存不够,可以用我们这个方式,只要用来理解原理,而不是真正的实际应用。
3. 关于示例里被注释掉的 system_message(重要说明)
你可能注意到,示例里有一行被我暂时注释掉了:
scss
# SystemMessage(content=system_message),
这是一个教学上的刻意不完整。
目的是让你对比:
- 有系统约束时,模型是否老老实实
- 没有约束时,模型会不会开始自由发挥
在你自己真正使用时, 这一行一定要加回去。
五、到这里为止,你其实已经完成了什么?
虽然我们还没接向量数据库, 但你已经完成了三件非常关键的事:
- 学会用 LangChain 控制上下文
- 学会约束模型的输出格式
- 学会限制模型的信息来源
合在一起,这才叫:
可控的大模型应用开发。
六、下一篇我们要做什么?
下一篇,才会真正进入 RAG 的「R」:
- 问题噪声的处理
- 文档切分
- Embedding
- 向量检索
- 把"检索结果"自动塞回 Context
但我们现在这个阶段,非常好的一点是:
咱们不是在堆概念,而是在一步步"驯化模型"。
这条路是慢一点,但非常扎实。
总结
到这一篇为止:
- 你已经能本地跑模型
- 能用 LangChain 控制对话
- 能强制模型按格式输出
- 能限制模型只能基于给定知识回答
如果你是第一次接触 AI 应用开发, 走到这里,已经完全可以给自己一个肯定,虽然咱们这个教程写的挺慢的,但是对于小白来说, 还是非常友好的,一步步学习,从原理上开始讲解,而不是一上来就让你用各种框架,你都不知道框架背后是发生了什么, 我们一起学习~ 一起加油吧~