欢迎光临
我们一直在努力

一个基于Streamlit​框架开发的本地化智能助手系统

一、程序概述与核心定位

一个基于Streamlit​框架开发的本地化智能助手系统,集成了身份认证、多格式文档处理、上下文感知对话、会话管理等功能,支持用户通过上传 PDF/TXT/DOCX 文档构建私有知识库,并基于大语言模型(LLM)实现文档问答与通用对话。其核心价值在于:将通用 LLM 能力与本地私有文档结合,同时通过轻量级身份验证保障访问安全,适用于实验室、小团队的内部知识管理与智能问答场景。

程序采用模块化设计,分为配置初始化、身份验证、客户端配置、文档处理、侧边栏管理、聊天界面六大功能模块,通过 Streamlit 的 session_state实现会话级数据持久化,整体架构轻量但完整,可直接部署于本地服务器或单机环境。

二、功能模块详解

(一)配置与初始化模块

1. 页面基础配置

st.set_page_config(
page_title="labAI 智能助手",
layout="wide",
initial_sidebar_state="expanded"
)

  • 作用:设置 Streamlit 应用的全局属性,layout="wide"启用宽屏布局,适配多列组件;initial_sidebar_state="expanded"默认展开侧边栏,便于用户操作文档和查看统计。

  • 技术细节:Streamlit 的 set_page_config必须在其他 Streamlit 命令前调用,此处作为程序入口的第一行配置,确保页面样式统一。

(二)身份验证模块

1. 用户凭证加载(load_users函数)

def load_users():
users = {}
try:
if os.path.exists("auth_users.txt"):
with open("auth_users.txt", "r") as f:
for line in f:
if ":" in line:
user, pwd = line.strip().split(":", 1)
users[user] = pwd
except Exception as e:
st.error(f"凭证加载失败: {str(e)}")
return users

  • 功能:从本地文件 auth_users.txt加载用户凭证,文件格式为 用户名:密码(每行一条)。

  • 设计逻辑:采用本地文件存储凭证(非数据库),简化部署复杂度,适合小团队场景;通过 try-except捕获文件不存在或格式错误异常,避免程序崩溃。

  • 局限性:明文存储密码,未做哈希加密,生产环境需优化(如添加 hashlib加密)。

2. 登录界面(login_form函数)

def login_form():
st.title("lab 身份验证")
with st.form("auth"):
user = st.text_input("工号/学号")
password = st.text_input("密码", type="password")
if st.form_submit_button("登录"):
users = load_users()
if users.get(user) == password:
st.session_state.logged_in = True
st.session_state.user = user
st.rerun()
else:
st.error("验证失败,请检查凭证")
st.stop()

  • 交互流程:通过 Streamlit 表单(st.form)封装输入框和提交按钮,提交后校验凭证:若匹配成功,设置 st.session_state.logged_in = True并记录用户名,通过 st.rerun()刷新页面进入主界面;失败则提示错误。

  • 关键机制:st.stop()确保未登录用户无法访问后续功能,强制停留在登录页。

3. 登录状态校验

if not st.session_state.get("logged_in"):
login_form()

  • 作用:程序启动时的第一道关卡,通过 Streamlit 会话状态(session_state)判断用户是否已登录。session_state是 Streamlit 用于跨页面刷新存储数据的核心机制,此处实现“会话级身份验证”。

(三)客户端配置模块

client = openai.OpenAI(
base_url="",
api_key="",
http_client=httpx.Client()
)

  • 功能:初始化 OpenAI 兼容的 API 客户端,用于调用大语言模型。

  • 参数说明:

    • base_url:LLM 服务地址(留空需用户手动填写,可对接本地部署的 QwQ-32B 或其他兼容 OpenAI API 的模型);

    • api_key:API 密钥(实际使用时需填入真实密钥);

    • http_client=httpx.Client():自定义 HTTP 客户端,支持超时、代理等高级配置。

  • 扩展性:通过修改 base_url,可无缝切换至不同 LLM 服务(如 OpenAI、本地 Ollama 服务等)。

(四)文档处理模块(核心功能之一)

1. 通用文本提取函数(extract_text)

def extract_text(file):
try:
# 临时文件创建
suffix = { … }.get(file.type, "")
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
tmp.write(file.getvalue())
tmp_path = tmp.name

# 按类型解析
if file.type == "application/pdf":
reader = PdfReader(tmp_path)
text = "\\n".join([p.extract_text() for p in reader.pages])
elif file.type == "text/plain":
with open(tmp_path, "r", encoding="utf-8") as f:
text = f.read()
elif file.type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
doc = Document(tmp_path)
text = "\\n".join([p.text for p in doc.paragraphs])
else:
return None

os.unlink(tmp_path) # 清理临时文件
return text[:15000] # 限制长度
except Exception as e:
st.error(f"文档解析失败: {str(e)}")
return None

  • 功能:支持 PDF、TXT、DOCX 三种格式的文档文本提取,返回纯文本内容。

  • 实现逻辑:

  • 临时文件处理:由于 PyPDF2、python-docx 等库需文件路径而非字节流,通过 tempfile.NamedTemporaryFile创建临时文件,写入上传文件的二进制数据,避免磁盘残留。

  • 格式适配:

    • PDF:使用 PyPDF2.PdfReader逐页提取文本,合并为字符串;

    • TXT:直接读取 UTF-8 编码文本;

    • DOCX:通过 python-docx.Document遍历段落(paragraphs)提取文本。

  • 安全限制:返回文本截断至前15000字符,防止过长上下文导致 LLM 调用超时或超 token 限制。

  • 异常处理:捕获所有解析异常,通过 st.error提示用户,避免程序崩溃。

(五)侧边栏管理模块

侧边栏是用户与程序交互的核心入口,集成文档管理、会话统计、系统控制三大功能:

def sidebar_manager():
with st.sidebar:
# 用户信息
st.write(f"用户:`{st.session_state.user}`")

# 文档管理(上传、展示、删除)
st.header("文档管理")
uploaded_files = st.file_uploader(…) # 多文件上传组件

# 处理新上传文件(提取文本并存入 session_state)
if uploaded_files:
for f in uploaded_files:
if f.name not in [d["name"] for d in st.session_state.get("docs", [])]:
content = extract_text(f)
if content:
st.session_state.docs.append({…}) # 存储文档元数据

# 显示文档列表(含删除按钮)
if st.session_state.get("docs"):
for doc in st.session_state.docs:
cols = st.columns([6, 2])
cols[0].caption(f"▪ {doc['name']} ({doc['size'] // 1024}KB)")
if cols[1].button("×", key=f"del_{doc['name']}"):
st.session_state.docs = [d for d in st.session_state.docs if d["name"] != doc["name"]]
st.rerun()

# 对话统计
st.header("对话统计")
msg_counts = {
"user": len([m for m in st.session_state.messages if m["role"] == "user"]),
"assistant": len([m for m in st.session_state.messages if m["role"] == "assistant"])
}
st.metric("用户消息", msg_counts["user"])
st.metric("助手回复", msg_counts["assistant"])

# 管理功能(清空对话、退出登录)
st.header("⚙管理")
if st.button("清空对话"):
st.session_state.messages = [st.session_state.messages[0]] if st.session_state.messages else []
st.rerun()
if st.button("退出登录"):
st.session_state.clear()
st.rerun()

  • 文档管理:

    • 去重机制:通过检查文件名是否已存在于 st.session_state.docs,避免重复上传同一文档;

    • 动态更新:删除文档时通过 st.rerun()实时刷新界面,确保状态同步。

  • 会话统计:基于 st.session_state.messages统计用户与助手的消息数量,直观展示对话活跃度。

  • 会话控制:清空对话保留系统提示(第一条消息),退出登录清除所有会话状态,返回登录页。

(六)聊天界面模块(核心功能之二)

1. 上下文提示构建(build_prompt函数)

def build_prompt(user_input):
context = []
if st.session_state.get("docs"):
context.append("## 参考文档")
for i, doc in enumerate(st.session_state.docs, 1):
context.append(f"【文档{i}】{doc['name']}\\n内容摘要:{doc['content'][:2000]}…")
context.append(f"## 用户问题\\n{user_input}")
return "\\n\\n".join(context)

  • 作用:将用户问题与上传文档内容拼接为 LLM 可理解的上下文提示,实现“基于文档的问答”。

  • 设计逻辑:

    • 若存在上传文档,优先插入文档摘要(截断至前 2000 字符,避免超 token 限制);

    • 若无文档,仅保留用户问题,由 LLM 使用通用知识库回答。

  • 优势:通过结构化提示(Markdown 标题分隔),引导 LLM 区分“参考文档”与“用户问题”,提升回答准确性。

2. 系统提示设置(可配置)

with st.expander("⚙ 系统设置", expanded=False):
default_prompt = """你是一个学术助手,根据以下规则响应:
1. 当存在参考文档时,严格基于文档内容回答
2. 没有文档时使用通用知识库
3. 回答语言与提问语言一致"""
sys_prompt = st.text_area("系统指令", value=…, height=150)
# 快捷模式按钮(恢复默认、文档模式、通用模式)
if cols[0].button("恢复默认"): …
if cols[1].button("文档模式"): …
if cols[2].button("通用模式"): …
if st.button("保存设置"):
st.session_state.messages[0]["content"] = sys_prompt # 更新系统提示

  • 功能:允许用户自定义 LLM 的系统指令,控制回答风格。系统提示存储在 st.session_state.messages[0](会话消息的第一条),确保每次调用 LLM 时生效。

3. 消息显示与用户输入

# 历史消息展示
for msg in st.session_state.messages[1:]:
role = msg["role"]
time = datetime.fromisoformat(msg["timestamp"]).strftime("%m/%d %H:%M")
with st.chat_message(role):
st.markdown(f"`[{time}]`\\n{msg['content']}")
if role == "user" and "docs_used" in msg:
st.caption(f"关联文档:{', '.join(msg['docs_used']) or '无'}")

# 用户输入处理
if prompt := st.chat_input("输入问题…"):
full_prompt = build_prompt(prompt)
user_msg = {
"role": "user",
"content": prompt,
"full_prompt": full_prompt, # 存储带上下文的完整提示
"timestamp": datetime.now().isoformat(),
"docs_used": [d["name"] for d in st.session_state.get("docs", [])]
}
st.session_state.messages.append(user_msg)

  • 消息结构:每条消息包含 role(角色)、content(内容)、timestamp(时间戳)、full_prompt(完整上下文,仅用户消息)、docs_used(关联的文档名)。

  • 用户体验:显示消息发送时间、关联文档,增强对话可追溯性。

4. LLM 调用与流式输出

response = client.chat.completions.create(
model="QwQ-32B", # 模型名称(可替换)
messages=[{"role": m["role"], "content": m["full_prompt"] if m["role"] == "user" else m["content"]}
for m in [st.session_state.messages[0]] + st.session_state.messages[1:]],
stream=True, # 流式输出
timeout=45 # 超时时间(秒)
)

# 流式渲染
response_text = ""
container = st.empty()
for chunk in response:
if chunk.choices[0].delta.content:
response_text += chunk.choices[0].delta.content
container.markdown(response_text + "▌") # 实时显示光标
container.markdown(response_text) # 最终输出

  • 流式调用:通过 stream=True启用流式响应,逐块接收 LLM 输出并实时渲染,避免用户等待过久;

  • 消息拼接:将系统提示(messages[0])与历史对话拼接为完整上下文,确保 LLM 理解对话历史;

  • 超时控制:设置 45 秒超时,防止因网络或模型响应慢导致的页面卡死。

三、数据结构设计

(一)会话状态(st.session_state)核心结构

键名

类型

描述

logged_in

bool

登录状态(True/False)

user

str

当前用户名(如工号/学号)

docs

list[dict]

上传文档列表,每个元素为文档字典:
{"name": 文件名, "content": 提取文本, "size": 文件大小(字节), "time": 上传时间}

messages

list[dict]

对话历史,每个元素为消息字典:
系统消息:{"role": "system", "content": 系统提示}
用户消息:{"role": "user", "content": 原始问题, "full_prompt": 带上下文的完整提示, "timestamp": ISO时间, "docs_used": [文档名列表]}
助手消息:{"role": "assistant", "content": 回答内容, "timestamp": ISO时间}

(二)临时数据结构

  • 用户凭证字典:users = {"user1": "pwd1", "user2": "pwd2"},由 load_users从文件加载;

  • 文档解析临时路径:tmp_path,通过 tempfile.NamedTemporaryFile生成,使用后自动删除。

四、核心算法与逻辑

(一)文档去重算法

if f.name not in [d["name"] for d in st.session_state.get("docs", [])]:
# 处理新文档

  • 逻辑:通过列表推导式遍历已上传文档的名称列表,判断新上传文件的文件名是否存在,实现简单高效的去重。时间复杂度 O(n),适合小规模文档场景。

(二)上下文窗口管理

  • 文档内容截断:单文档提取文本限制为 15000 字符,多文档时每个文档摘要限制为 2000 字符,总上下文长度控制在 LLM 可接受范围内(如 QwQ-32B 通常支持 32k tokens);

  • 历史消息保留:完整保留对话历史(除清空操作外),通过 LLM 的上下文窗口自然实现多轮对话记忆。

  • (三)流式输出渲染算法

    response_text = ""
    container = st.empty() # 创建空容器
    for chunk in response:
    if chunk.choices[0].delta.content:
    response_text += chunk.choices[0].delta.content
    container.markdown(response_text + "▌") # 追加光标
    container.markdown(response_text) # 移除光标,显示最终结果

    • 原理:利用 Streamlit 的 st.empty()创建可更新的容器,每接收到一个 chunk 就更新容器内容,模拟打字机效果,提升用户体验。

    五、程序执行流程

  • 启动初始化:设置页面配置 → 检查登录状态 → 未登录则显示登录页;

  • 登录验证:用户输入凭证 → 加载 auth_users.txt校验 → 成功后进入主界面;

  • 主界面加载:初始化 session_state.messages(若为空)→ 渲染侧边栏(用户信息、文档管理、统计)→ 渲染聊天界面;

  • 文档上传:用户上传文件 → 调用 extract_text提取文本 → 存入 session_state.docs;

  • 对话交互:用户输入问题 → build_prompt构建带文档上下文的提示 → 调用 LLM API(流式)→ 实时渲染回答 → 存入对话历史;

  • 会话管理:支持清空对话(保留系统提示)、退出登录(清除所有状态)。

  • 六、技术特点与优化建议

    (一)技术特点

  • 轻量级部署:依赖少(仅需 Streamlit、PyPDF2、python-docx 等),无需数据库,适合本地或小团队快速部署;

  • 模块化设计:各功能模块解耦,便于维护扩展(如新增文档格式支持、更换 LLM 模型);

  • 会话级隔离:通过 session_state实现多用户会话隔离,每个用户拥有独立的文档库和对话历史;

  • 用户体验优化:流式输出、实时统计、文档关联提示等细节提升易用性。

  • (二)优化建议

  • 安全性增强:

    • 密码哈希存储(如使用 bcrypt替代明文);

    • 限制上传文件大小(当前仅通过 file.size显示,未做拦截);

  • 性能优化:

    • 文档文本缓存(避免重复解析同一文档);

    • 异步处理大文档(当前同步解析可能导致页面卡顿);

  • 功能扩展:

    • 支持更多文档格式(如 Markdown、Excel);

    • 添加文档检索(如基于向量数据库的语义搜索,替代全文拼接);

    • 导出对话历史(TXT/PDF)。

  • 七、总结

    一个功能完整、架构清晰的本地化智能助手系统,核心价值在于将私有文档​与大语言模型​有机结合,同时通过轻量级身份验证保障安全。其设计兼顾了易用性与扩展性,既适合实验室内部知识管理,也可作为二次开发的基础框架。通过优化安全性和性能,可进一步应用于更复杂的场景(如企业知识库、教育辅助系统等)。

    源代码

    import streamlit as st
    import openai
    import httpx
    import json
    import os
    import tempfile
    from datetime import datetime
    from PyPDF2 import PdfReader
    from docx import Document

    # ————————–
    # 配置与初始化
    # ————————–
    st.set_page_config(
    page_title="labAI 智能助手",
    layout="wide",
    initial_sidebar_state="expanded"
    )

    # ————————–
    # 身份验证模块
    # ————————–
    def load_users():
    """加载用户凭证"""
    users = {}
    try:
    if os.path.exists("auth_users.txt"):
    with open("auth_users.txt", "r") as f:
    for line in f:
    if ":" in line:
    user, pwd = line.strip().split(":", 1)
    users[user] = pwd
    except Exception as e:
    st.error(f"凭证加载失败: {str(e)}")
    return users

    def login_form():
    """显示登录界面"""
    st.title("lab 身份验证")
    with st.form("auth"):
    user = st.text_input("工号/学号")
    password = st.text_input("密码", type="password")
    if st.form_submit_button("登录"):
    users = load_users()
    if users.get(user) == password:
    st.session_state.logged_in = True
    st.session_state.user = user
    st.rerun()
    else:
    st.error("验证失败,请检查凭证")
    st.stop()

    # ————————–
    # 登录检查
    # ————————–
    if not st.session_state.get("logged_in"):
    login_form()

    # ————————–
    # 客户端配置
    # ————————–
    client = openai.OpenAI(
    base_url="",
    api_key="",
    http_client=httpx.Client()
    )

    # ————————–
    # 文档处理模块
    # ————————–
    def extract_text(file):
    """通用文本提取函数"""
    try:
    # 创建临时文件
    suffix = {
    "application/pdf": ".pdf",
    "text/plain": ".txt",
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx"
    }.get(file.type, "")

    with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
    tmp.write(file.getvalue())
    tmp_path = tmp.name

    # 按类型解析
    if file.type == "application/pdf":
    reader = PdfReader(tmp_path)
    text = "\\n".join([p.extract_text() for p in reader.pages])
    elif file.type == "text/plain":
    with open(tmp_path, "r", encoding="utf-8") as f:
    text = f.read()
    elif file.type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
    doc = Document(tmp_path)
    text = "\\n".join([p.text for p in doc.paragraphs])
    else:
    return None

    os.unlink(tmp_path) # 清理临时文件
    return text[:15000] # 限制长度

    except Exception as e:
    st.error(f"文档解析失败: {str(e)}")
    return None

    # ————————–
    # 侧边栏组件
    # ————————–
    def sidebar_manager():
    """侧边栏管理功能"""
    with st.sidebar:
    st.write(f"用户:`{st.session_state.user}`")

    # 文档管理
    st.header("文档管理")
    uploaded_files = st.file_uploader(
    "上传文档 (PDF/TXT/DOCX)",
    type=["pdf", "txt", "docx"],
    accept_multiple_files=True
    )

    # 处理新上传文件
    if uploaded_files:
    for f in uploaded_files:
    if f.name not in [d["name"] for d in st.session_state.get("docs", [])]:
    content = extract_text(f)
    if content:
    if "docs" not in st.session_state:
    st.session_state.docs = []
    st.session_state.docs.append({
    "name": f.name,
    "content": content,
    "size": f.size,
    "time": datetime.now().strftime("%m/%d %H:%M")
    })

    # 显示文档列表
    if st.session_state.get("docs"):
    st.subheader("已加载文档")
    for doc in st.session_state.docs:
    cols = st.columns([6, 2])
    cols[0].caption(f"▪ {doc['name']} ({doc['size'] // 1024}KB)")
    if cols[1].button("×", key=f"del_{doc['name']}"):
    st.session_state.docs = [d for d in st.session_state.docs if d["name"] != doc["name"]]
    st.rerun()

    # 统计信息
    st.header(" 对话统计")
    msg_counts = {
    "user": len([m for m in st.session_state.messages if m["role"] == "user"]),
    "assistant": len([m for m in st.session_state.messages if m["role"] == "assistant"])
    }
    st.metric("用户消息", msg_counts["user"])
    st.metric("助手回复", msg_counts["assistant"])

    # 管理功能
    st.header("⚙管理")
    if st.button("清空对话"):
    st.session_state.messages = [st.session_state.messages[0]] if st.session_state.messages else []
    st.rerun()

    if st.button(" 退出登录"):
    st.session_state.clear()
    st.rerun()

    # ————————–
    # 聊天界面
    # ————————–
    def build_prompt(user_input):
    """构建带上下文的提示"""
    context = []
    if st.session_state.get("docs"):
    context.append("## 参考文档")
    for i, doc in enumerate(st.session_state.docs, 1):
    context.append(f"【文档{i}】{doc['name']}\\n内容摘要:{doc['content'][:2000]}…")
    context.append(f"## 用户问题\\n{user_input}")
    return "\\n\\n".join(context)

    def chat_interface():
    """主聊天界面"""
    st.title(" labAI 智能助手")

    # 系统提示设置
    with st.expander("⚙ 系统设置", expanded=False):
    default_prompt = """你是一个学术助手,根据以下规则响应:
    1. 当存在参考文档时,严格基于文档内容回答
    2. 没有文档时使用通用知识库
    3. 回答语言与提问语言一致"""

    sys_prompt = st.text_area(
    "系统指令",
    value=st.session_state.messages[0]["content"] if st.session_state.messages else default_prompt,
    height=150
    )

    cols = st.columns(3)
    if cols[0].button("恢复默认"):
    sys_prompt = default_prompt
    if cols[1].button("文档模式"):
    sys_prompt = "严格基于提供的文档内容回答问题,超出文档范围时明确说明"
    if cols[2].button("通用模式"):
    sys_prompt = "使用通用知识库回答问题,保持简洁专业"

    if st.button("保存设置"):
    if not st.session_state.messages:
    st.session_state.messages = [{"role": "system", "content": sys_prompt}]
    else:
    st.session_state.messages[0]["content"] = sys_prompt
    st.rerun()

    # 消息显示
    for msg in st.session_state.messages[1:]:
    role = msg["role"]
    time = datetime.fromisoformat(msg["timestamp"]).strftime("%m/%d %H:%M")
    with st.chat_message(role):
    st.markdown(f"`[{time}]`\\n{msg['content']}")
    if role == "user" and "docs_used" in msg:
    st.caption(f"关联文档:{', '.join(msg['docs_used']) or '无'}")

    # 用户输入
    if prompt := st.chat_input("输入问题…"):
    full_prompt = build_prompt(prompt)

    user_msg = {
    "role": "user",
    "content": prompt,
    "full_prompt": full_prompt,
    "timestamp": datetime.now().isoformat(),
    "docs_used": [d["name"] for d in st.session_state.get("docs", [])]
    }
    st.session_state.messages.append(user_msg)

    # 显示消息
    with st.chat_message("user"):
    st.markdown(prompt)
    if user_msg["docs_used"]:
    st.caption(f" 已关联 {len(user_msg['docs_used'])} 个文档")

    # 生成回答
    with st.chat_message("assistant"):
    try:
    response = client.chat.completions.create(
    model="QwQ-32B",
    messages=[
    {"role": m["role"], "content": m["full_prompt"] if m["role"] == "user" else m["content"]}
    for m in [st.session_state.messages[0]] + st.session_state.messages[1:]
    ],
    stream=True,
    timeout=45
    )

    response_text = ""
    container = st.empty()
    for chunk in response:
    if chunk.choices[0].delta.content:
    response_text += chunk.choices[0].delta.content
    container.markdown(response_text + "▌")

    container.markdown(response_text)
    st.session_state.messages.append({
    "role": "assistant",
    "content": response_text,
    "timestamp": datetime.now().isoformat()
    })

    except Exception as e:
    st.error(f"生成失败: {str(e)}")

    # ————————–
    # 主程序
    # ————————–
    if __name__ == "__main__":
    # 初始化会话状态
    if "messages" not in st.session_state:
    st.session_state.messages = []

    # 界面布局
    sidebar_manager()
    chat_interface()

    赞(0)
    未经允许不得转载:171主机测评 » 一个基于Streamlit​框架开发的本地化智能助手系统
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址