【从0到1大模型应用开发实战】02|用 LangChain 和本地大模型,完成第一次“可控对话

前言

在上一篇文章里,我们只做了一件事:

在自己的电脑上,把大模型跑起来,并让它回一句话。

如果你能顺利跑完上一篇,其实已经超过了很多"只停留在概念层"的人。

这一篇,我们继续往前走一步。

不讲复杂原理,不讲论文,也暂时不做真正的 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="那它解决了什么问题?"),
]

这里做了两件很重要的事:

  1. 你明确告诉模型:它的角色是什么
  2. 你把"上一轮对话"显式塞给了模型

如果你后面做过多轮问答、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),

这是一个教学上的刻意不完整

目的是让你对比:

  • 有系统约束时,模型是否老老实实
  • 没有约束时,模型会不会开始自由发挥

在你自己真正使用时, 这一行一定要加回去。

五、到这里为止,你其实已经完成了什么?

虽然我们还没接向量数据库, 但你已经完成了三件非常关键的事:

  1. 学会用 LangChain 控制上下文
  2. 学会约束模型的输出格式
  3. 学会限制模型的信息来源

合在一起,这才叫:

可控的大模型应用开发。

六、下一篇我们要做什么?

下一篇,才会真正进入 RAG 的「R」:

  • 问题噪声的处理
  • 文档切分
  • Embedding
  • 向量检索
  • 把"检索结果"自动塞回 Context

但我们现在这个阶段,非常好的一点是:

咱们不是在堆概念,而是在一步步"驯化模型"。

这条路是慢一点,但非常扎实。

总结

到这一篇为止:

  • 你已经能本地跑模型
  • 能用 LangChain 控制对话
  • 能强制模型按格式输出
  • 能限制模型只能基于给定知识回答

如果你是第一次接触 AI 应用开发, 走到这里,已经完全可以给自己一个肯定,虽然咱们这个教程写的挺慢的,但是对于小白来说, 还是非常友好的,一步步学习,从原理上开始讲解,而不是一上来就让你用各种框架,你都不知道框架背后是发生了什么, 我们一起学习~ 一起加油吧~

相关推荐
爱吃牛肉的大老虎2 小时前
Spring WebFlux与SpringMVC 对比讲解
java·后端·spring
老华带你飞2 小时前
房屋租赁管理系统|基于java+ vue房屋租赁管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
superman超哥3 小时前
仓颉性能优化秘籍:内联函数的优化策略与深度实践
开发语言·后端·性能优化·内联函数·仓颉编程语言·仓颉·仓颉语言
IT_陈寒3 小时前
Vue3性能优化实战:7个被低估的Composition API技巧让渲染提速40%
前端·人工智能·后端
superman超哥3 小时前
仓颉编译器优化揭秘:尾递归优化的原理与实践艺术
开发语言·后端·仓颉编程语言·仓颉·仓颉语言·尾递归·仓颉编译器
自由生长20243 小时前
保障缓存和数据库尽量一致的策略
后端
韭菜炒大葱4 小时前
LangChain 二:输出结果定制与历史管理能力详解
前端·langchain·openai
海南java第二人4 小时前
Spring Bean作用域深度解析:从单例到自定义作用域的全面指南
java·后端·spring
悟空码字4 小时前
SpringBoot 整合 Nacos,让微服务像外卖点单一样简单
java·spring boot·后端