跳到主要内容

让别人也能打开它 — 一个网页、一个坑、两个文件

这一章讲三件事: 不会写网页怎么做界面;为什么第一版只能问一句就断; 以及上线到底要推什么东西。

它在全书链条里的位置: 这是第 01 章那张八步图里的第 ⑦ 步。 前面八章的成果都锁在 Notebook 里,别人看不见。这一章把它们放出来。

本章主走查:一个叫 streamlit_app.py 的文件。 它会从一句命令变成浏览器里的一个输入框,再被加上一段代码变成能连续对话的界面。 下面的每一段代码都是原书里的,行数与结构照录。

1. 现在这个东西只有你自己能用

这一节先说清这一章要解决的问题。

到第 08 章为止,你手上有的是一个能跑的 Notebook—— 一个把代码和运行结果混排在一起的编辑器,书全程用它教学。 你要给别人看,只能让他装环境、装依赖、拿到密钥、逐个格子按运行。

书对这件事的说法是:我们对知识库和模型已经有了基本的理解, 现在是时候把它们打造成一个富有视觉效果的界面了——这样不仅操作更便捷,还能便于与他人分享1

2. 不用学网页那三样,写普通 Python 就行

这一节介绍这一章唯一的新工具,以及它的代价。

书用的工具叫 Streamlit——一个用来快速把 Python 脚本变成网页的库。 它的卖点书写得很直白:只需要编写普通的 Python 模块, 就可以在很短的时间内创建美观并具备高度交互性的界面, 而无需编写任何前端、网页或 JavaScript 代码2

它和另一类工具的区别书也点了: 和那些只能靠拖拽生成界面的工具不同, 你仍然具有对代码的完整控制权2

它提供的东西就是一组函数,一个函数画一样东西3:

你写的页面上出现什么
st.title() / st.header()大标题 / 小标题
st.write() / st.markdown()一段文字、一张表、一张图
st.text_input() / st.slider() / st.selectbox()输入框 / 滑块 / 下拉选择
st.button() / st.radio() / st.checkbox()按钮 / 单选 / 复选
st.info()一个蓝色的提示框

代价是:你只能用它给的这些零件,长什么样由它定。 换句话说,你换来的是速度,付出的是外观上的自由。

3. 第一版:六步,一个能问一句的页面

这一节是主走查的第 1 步。

书给的步骤是六步,合起来是一个文件4:

import streamlit as st
from langchain_openai import ChatOpenAI

st.title('🦜🔗 动手学大模型应用开发') # ② 标题

openai_api_key = st.sidebar.text_input('OpenAI API Key',
type='password') # ③ 侧边栏输密钥

def generate_response(input_text): # ④ 拿到回答
llm = ChatOpenAI(temperature=0.7, openai_api_key=openai_api_key)
st.info(llm(input_text)) # 显示在蓝框里

with st.form('my_form'): # ⑤ 一个表单
text = st.text_area('Enter text:', '……')
submitted = st.form_submit_button('Submit')
if not openai_api_key.startswith('sk-'):
st.warning('Please enter your OpenAI API key!', icon='⚠')
if submitted and openai_api_key.startswith('sk-'):
generate_response(text)

跑起来只要一句命令4:

streamlit run streamlit_app.py

图说:这一句之后,浏览器里就出现了一个标题、一个侧边栏的密钥输入框、
一个文本框和一个提交按钮。全程没有写过一行网页代码。

注意第 ③ 步那个 type='password': 它让输入框显示成圆点而不是明文。 这是这一章唯一一处安全考虑,而它远远不够——第 8 节会说为什么。

4. 那个坑:每次点击,整个脚本从头跑一遍

这一节讲这一章唯一需要真正理解的机制。

书对第一版的评价只有一句:但是当前只能进行单轮对话5「只能单轮」不是功能没做,是被这个工具的运行方式挡住了。

它的运行方式是这样的:

用户点了「Submit」

└─→ 整个 streamlit_app.py 从第一行重新执行一遍
├─ import 重新跑
├─ st.title 重新画
├─ 所有普通变量重新初始化 ← 上一轮的对话记录就死在这里
└─ 画出新的页面

图说:普通 Python 变量活不过一次点击。
你在函数里存的那份历史,下一次点击时它已经被重新赋成空的了。

这就是第 08 章那个「记忆」在网页上失效的原因: 盒子还在,但装盒子的那个变量(程序里用来存一个值、随时能改的那个名字)每次都被重建了。

5. 治法:一个专门活下来的盒子

这一节是主走查的第 2 步。

书给的办法是 st.session_state—— 它是这个工具提供的一个专门的存放处,里面的东西不会被重跑清掉6。 书的说法是:用它来存对话历史,可以在用户与应用程序交互时保留整个对话的上下文6

改法只有三处6:

# ① 第一次运行时建一个空列表,以后不再重建
if 'messages' not in st.session_state:
st.session_state.messages = []

if prompt := st.chat_input("Say something"):
# ② 用户这句话追加进去
st.session_state.messages.append({"role": "user", "text": prompt})

answer = generate_response(prompt, openai_api_key)
if answer is not None:
# ③ 模型的回答也追加进去
st.session_state.messages.append({"role": "assistant", "text": answer})

# 每次重跑时,把整份历史重画一遍
for message in st.session_state.messages:
...

第 ① 行那个 if 是全部的机关: 只在盒子里没有这一项时才建空列表。 第二次重跑时条件不成立,于是上一轮存进去的东西原样留着。

最后那个循环也要看一眼: 因为整页每次都重画, 你必须把全部历史重新画一遍,否则页面上只会剩最新一句。

6. 把前面几章接进来:三个函数,三挡开关

这一节讲这个页面怎么和第 06、07、08 章连上。

书把前面的成果封装成三个函数7:

函数干什么
get_vectordb按路径打开第 06 章那个库
get_qa_chain第 07 章那条链:检索问答,不带历史
get_chat_qa_chain第 08 章那条链:检索问答,带历史

然后在页面上加一个三挡单选8:

( ) None 不使用检索问答的普通模式
( ) qa_chain 不带历史记录的检索问答模式
( ) chat_qa_chain 带历史记录的检索问答模式

图说:这三挡正好对应第 02 章、第 07 章、第 08 章。
同一个页面上并排放着,用户自己切换就能看出差别——
这其实是这本书里最好的一次现场对照实验,可惜书没有拿它做实验。

7. 上线:推两个文件

这一节讲最后一步,它比想象的短。

书给的部署步骤是三步9:

① 建一个代码仓库,里面放两个文件:
your-repository/
├── streamlit_app.py ← 你的程序
└── requirements.txt ← 这个程序要装哪些依赖
② 在 Streamlit Community Cloud 上点 New app,指定仓库、分支、主文件路径
③ 点 Deploy

图说:没有服务器,没有域名,没有运维。
第二步还能自定义子域名,于是你的应用就有了一个可以发给别人的网址。

注意 requirements.txt 那一行——它是这一步唯一容易翻车的地方。 本机能跑不代表云上能跑:本机那些你早就装好、却没写进这份清单的包,云上一个都没有。

8. 边界:书自己列了三条,我们再补一条

这一节是本章的边界,前三条是书的,最后一条是我们的。

书自己承认这个成品是简化过的,并列了三个还可以往下做的方向10:

书说还可以加为什么
界面里上传本地文档、当场建库现在的库是提前建好的,用户换不了资料
多种模型与向量模型的选择按钮现在写死了一家
修改参数的按钮温度、取几块这些现在都是写死的

我们要补的第四条,书一个字没提:那个密钥输入框是一个隐患。

第一,它把密钥交给了页面。 密钥输在浏览器里,意味着每个使用者都要有自己的密钥—— 这对演示可以,对产品不行(你不能要求用户去 OpenAI 注册)。

第二,更要紧的是第 03 章那道墙。 这个页面把用户输入的任何文字直接送进提示词, 而第 03 章演过:一句「忽略之前的文本」就能顶掉你的整段指令。 这个页面没有任何分隔符,没有任何过滤。

判断(我们的,不是书里的): 这一章的技术内容其实只有一条—— 「脚本每次重跑,所以要有一个专门活下来的盒子」;其余都是照着文档抄函数名。 但这一条值钱,因为它是所有这类「把脚本变成网页」的工具共有的模型, 换一个工具,你要问的第一个问题仍然是「什么东西活得过一次点击」。 如果错,会错在: 如果你用的是传统网页框架(前后端分开的那种), 那么这条根本不存在——那边是服务端常驻、前端自己管状态,完全另一套心智。

9. 可带走的

  1. 做界面不用学网页那三样,写普通 Python 就行;代价是零件由它定,外观不自由;
  2. 一个标题、一个输入框、一个按钮、一次回答,加起来不到二十行;
  3. 跑起来只要一句 streamlit run streamlit_app.py;
  4. 最要紧的机制:每次交互,整个脚本从第一行重跑一遍——普通变量活不过一次点击;
  5. 所以第一版只能单轮对话,不是功能没做,是历史被重跑清掉了;
  6. 治法是把该活下来的东西放进 st.session_state,并且用 if not in 保证只建一次;
  7. 因为整页每次重画,你必须把全部历史重新画一遍;
  8. 三挡单选正好对应三章:不检索 / 检索不带历史 / 检索带历史;
  9. 上线只推两个文件:程序本身和依赖清单;依赖清单漏写是这一步最常见的翻车;
  10. 书自己列了三条往下做的方向:上传文档建库、换模型、改参数;
  11. 书没提的第四条隐患:页面把用户输入直接送进提示词,没有第 03 章那道墙

10. 原文地图

主题原书章原文位置
为什么要做界面部署知识库助手text/06-p101-120.txt:308(搜「富有视觉效果的界") · text/06-p101-120.txt:311(搜「Streamlit 是⼀种快速便捷的⽅法」)
不用写网页代码、有完整控制权部署知识库助手text/06-p101-120.txt:314(搜「⽆需编写任何前端」) · text/06-p101-120.txt:320(搜「不需要你去编写任何客户端代码」)
那组函数部署知识库助手text/06-p101-120.txt:326(搜「st.write()」) · text/06-p101-120.txt:344(搜「st.button()」)
第一版六步与跑起来的命令部署知识库助手text/06-p101-120.txt:363(搜「openai_api_key = st.sidebar.text_input」) · text/06-p101-120.txt:375(搜「st.form」) · text/06-p101-120.txt:388(搜「streamlit run streamlit_app.py」)
只能单轮部署知识库助手text/06-p101-120.txt:391(搜「当前只能进」)
那个活下来的盒子部署知识库助手text/06-p101-120.txt:392(搜「保留整个对话的上下文」) · text/06-p101-120.txt:403(搜「st.session_state.messages = []」)
三个函数与三挡单选部署知识库助手text/06-p101-120.txt:428(搜「get_vectordb函数返回」) · text/06-p101-120.txt:484(搜「单选按钮部件st.radio」)
部署三步、两个文件部署知识库助手text/06-p101-120.txt:509(搜「requirements.txt」) · text/06-p101-120.txt:511(搜「New app」)
书自己列的优化方向部署知识库助手text/06-p101-120.txt:518(搜「期待学习者」) · text/06-p101-120.txt:524(搜「添加多种LLM」)

Footnotes

  1. 出处:「部署知识库助手」第 308 至 311 段(text/06-p101-120.txt:308,搜「富有视觉效果的界」)。补充(不在书里,来自通用知识):Notebook 这个编辑器书在环境配置那一章介绍过(text/02-p21-40.txt:252,搜「交互式计算环境」),它的特点是把代码分成一个个格子,按一次运行一格,结果直接显示在格子下面。

  2. 出处:「部署知识库助手」第 314 与 317 至 322 段(text/06-p101-120.txt:314,搜「⽆需编写任何前端」;后半句见 text/06-p101-120.txt:320,搜「不需要你去编写任何客户端代码」)。原文对照的是 Flask/Django 这类常规网页框架。「换来速度、付出外观自由」这句权衡是我们的判断,书没有说。 2

  3. 出处:「部署知识库助手」第 324 至 347 段(text/06-p101-120.txt:326,搜「st.write()」;最后几个见 text/06-p101-120.txt:344,搜「st.button()」)。原文一共列了九组函数,本章合并成五行;没列的是画数据表、画图表那几组。

  4. 出处:「部署知识库助手」第 350 至 388 段(密钥输入框见 text/06-p101-120.txt:363,搜「openai_api_key = st.sidebar.text_input」;表单见 text/06-p101-120.txt:375,搜「st.form」;命令见 text/06-p101-120.txt:388,搜「streamlit run streamlit_app.py」)。代码块右侧那几个圆圈序号是我们加的,对应原文那六个步骤的编号;代码本身照录,只把文本框里那句默认提示省成了省略号。 2

  5. 出处:「部署知识库助手」第 391 段(text/06-p101-120.txt:391,搜「当前只能进」)。「不是功能没做,是被运行方式挡住了」这个诊断是我们的——书只说了现象,没有解释原因。那张「从头重跑」的图同样是我们画的。补充(不在书里,来自通用知识):这个工具的运行模型就是「任何交互触发整脚本重跑」,这一点写在它自己的官方文档里。

  6. 出处:「部署知识库助手」第 391 至 423 段(text/06-p101-120.txt:392,搜「保留整个对话的上下文」;那个 iftext/06-p101-120.txt:403,搜「st.session_state.messages = []」)。代码块里那三条注释是我们加的,原文的注释是「用于跟踪对话历史」「将用户输入添加到对话历史中」「显示整个对话历史」。 2 3

  7. 出处:「部署知识库助手」第 425 至 482 段(text/06-p101-120.txt:428,搜「get_vectordb函数返回」)。原文对三个函数的说明依次是:返回持久化后的向量知识库、返回调用带有历史记录的检索问答链后的结果、返回调用不带历史记录的检索问答链后的结果。

  8. 出处:「部署知识库助手」第 484 至 494 段(text/06-p101-120.txt:484,搜「单选按钮部件st.radio」)。三挡的说明文字照录。图说最后那句「可惜书没有拿它做实验」是我们的评论。

  9. 出处:「部署知识库助手」第 502 至 516 段(两个文件见 text/06-p101-120.txt:509,搜「requirements.txt」;第二步见 text/06-p101-120.txt:511,搜「New app」)。「依赖清单漏写是最常见的翻车」这句提醒是我们补的,书没有说。

  10. 出处:「部署知识库助手」第 518 至 526 段(text/06-p101-120.txt:518,搜「期待学习者」;三条方向见 text/06-p101-120.txt:524,搜「添加多种LLM」)。原文那三条之后还有一个「更多……」。第四条(密钥与那道墙)完全是我们补的,书这一章一个字没提安全。