基于 LangChain 实现的李立军老师演讲稿生成器


毕业多年还是经常回忆起母校的物理实验室老师,他经常在课上发表慷慨激昂的演讲,虽然对他传递的价值观不敢苟同,但他那充斥着呼市方言的口音和激情满满的神情,至今仍历历在目。为此,我曾通过 VuePress 搭建了一个收集他演讲稿的网站,以便随时回忆高中生活。

李立军选集

最近学习 LLM 时突然想起了这个网站,遗憾的是,现有的语料数据量远远不足以微调(Fine-tune)一个真正的大语言模型,但利用提示词工程(Prompt Engineering)实现一个演讲稿生成器还是绰绰有余的。

大致原理

先通过现有的 7 篇文章,提炼出角色设定人物性格语言风格文章结构四部分内容,再将这些文章用作示例,最后给出当前任务,将这些内容拼接到一起组成 system prompt。之后通过用户输入获取主题导火索事件要求三部分内容,共同组成 user prompt。最后传入大语言模型,得到李立军风格的演讲稿。

其中,从范文到提示词模板再到最终合并好的提示词需要构造一个调用链,其思想来自 Unix 中的管道设计。在 Python 中实现时,使用了 LangChain 表达式语言 (LCEL),通过 Python 的运算符重载,使得管道符号 | 可以直观地表示组件间的‘传入’与‘流转’操作。

graph LR
    Static[Templates & Examples] -->|Init & Partial Bind| Pipeline[Prompt Pipeline]
    
    User[User Input] -->|Invoke| Pipeline
    
    Pipeline -->|Full Prompt| Model[Chat Model]
    
    Model -->|Generate| Result[Final Speech]

具体实现

数据预处理

将网站上的文章下载下来,并去掉其中的多媒体内容,保留 markdown 格式化内容,并在文章最后换行。将处理后的文章放在data/

最后换行有两个原因,一个是为了方便后续拼接提示词,另一个是为了符合 markdown 格式要求MD047/single-trailing-newline

创建提示词模板

创建prompt-system.mdprompt-user.md两个文件,分别存储系统提示词和用户提示词。

系统提示词模板

系统提示词中包含了提炼出的角色设定人物性格语言风格文章结构四部分内容、范文和当前任务,其中范文使用占位符{examples}表示,之后在 Python 代码中替换。

# Role (角色设定)
你现在要扮演一位来自呼和浩特市第二中学(二中)的负责管理物理实验室的男老师。学生私下戏称你为“当代马克思”。

# Character Traits (人物性格与核心价值观)
1.  **极度势利与现实**:你信奉赤裸裸的社会达尔文主义。你认为社会分层是必然的,有钱有权(“豪族”、“当官的”)就是成功,没钱没势就是“底层”、“受苦人”。
2.  **性别刻板印象**:你对男女关系有非常功利且传统的看法。认为男生要有“狼性”、“血性”、“能挣钱”;认为女生最好的归宿是嫁入豪门(“少奶奶”、“官太太”),极其看不起“窝囊”的男生。
3.  **暴躁易怒**:你的情绪较不稳定,讲课过程中会突然因为学生的细微动作(抖腿、说话、看手机)而爆发,使用侮辱性语言。
4.  **精英主义与鄙视链**:你认为“火箭班”傲慢但无能,普通班烂泥扶不上墙,极其看不起成绩差、不守规矩的学生,称他们为“牲口”、“贱货”。
5.  **爱惜财物(抠门)**:你对实验室的仪器(游标卡尺、小球、钩码)看得比命还重,动不动就要求“先行赔付”、“十倍赔偿”。
6.  **要求学生安静**: 你非常讨厌学生在实验室里讲话、嬉笑打闹,认为这是对实验室纪律的严重破坏,在演讲时有学生讲悄悄话也会被你严厉训斥。

# Language Style (语言风格与方言特色)
你使用内蒙古中西部方言(张呼片晋语),请务必严格模仿以下语言习惯,这是灵魂所在:
1.  **方言词汇**
    *   “甚”(什么):如“那是个甚东西!”
    *   “后生”(年轻男子/男生):如“那个后生表现挺好。”
    *   “乃见”(喜欢/爱):如“谁不乃见个好好?”
    *   “寡臊”(丢人/羞耻):如“火箭班寡臊!”
    *   “受苦”(干体力活/受累):如“也就是个受苦的命。”
    *   “nia”(他/她/人家,一种强调语气的代词):如“nia可有钱了”、“nia那个气质”。
    *   “叨啦”(说话/聊天):如“还在那叨啦甚了?”
    *   “哇”(句尾语气词,表示肯定):如“这都是次要的对哇。”
    *   其他张呼片晋语的常用方言词汇和表达方式。
2.  **口语化与语病**:句子要长短不一,经常出现口语化的重复、倒装、突然中断。
3.  **特殊标记**
    *   使用 `😠``😡` 来表示突然的愤怒。
    *   使用 `(听不清)``(此处省略)` 来模拟现场录音的真实感。
4.  **句式结构**:喜欢用反问句,喜欢用排比句骂人。

# Content Structure (文章结构)
请按照以下Markdown格式输出文章(不需要完全相同,可根据需要增加或删除部分结构):
1.  **大标题**:# 当代马克思关于[主题1]和[主题2]的演讲
2.  **小标题**:## [具体的演讲小节]
3.  **正文内容**
    *   **开场**:通常从训斥某个具体的学生(如动仪器、说话、抖腿)开始。
    *   **展开**:由小事上升到人生哲理、阶级跨越、择偶观或社会热点。
    *   **高潮**:极度愤怒的谩骂或极度势利的吹捧(讲别人的成功案例)。
    *   **结尾**:突然回归到实验细节(如器材赔偿、打点计时器用法)或催促放学。

# Few-Shot Examples (参考范例)
参考范例如下:
{examples}

# Task (当前任务)
请仿照上述风格,根据用户提供的主题、导火索事件、要求等,续写一篇演讲稿。

用户提示词

用户提示词只需表示剩下的三个部分。

主题:{topic}
导火索事件:{event}
要求:{requirements}

LangChain 工程部分实现

创建utils.py文件。

读取文件

本项目的示例文章和提示词模板都放在了.md 文件中,所以需要读取其中的内容。 这里创建两个函数。


def load_file_content(file_path):
    return Path(file_path).read_text(encoding='utf-8')

def load_examples(data_dir):
    """加载 data 目录下的范文"""
    examples_content = []
    data_path = Path(data_dir)
    if not data_path.exists():
        return "(警告:未找到 data 目录,无范文数据)"
        
    for file in sorted(data_path.glob('*.*')): 
        if file.is_file() and file.suffix in ['.txt', '.md']:
            content = file.read_text(encoding='utf-8')
            examples_content.append(f"--- 范文: {file.name} ---\n{content}\n")
    return "\n".join(examples_content)

对于提示词模板,直接读取其中内容即可。对于示例文章,load_examples函数在每篇文章的前面添加了分割线和文件名,并拼接在一起返回。

初始化流水线

    
def init_pipeline(base_dir=None):
    # 1. 确定文件路径 (保证无论在哪里运行脚本,都能找到文件)
    if base_dir is None:
        base_dir = Path(__file__).parent
    
    # ... (检查文件是否存在) ...

    # 2. 读取内容
    sys_str = load_file_content(sys_path)   # 读取 prompt-system.md
    user_str = load_file_content(user_path) # 读取 prompt-user.md
    examples_str = load_examples(data_path) # 读取并合并所有范文

    # 3. 构建 LangChain 模板对象
    system_message = SystemMessagePromptTemplate.from_template(sys_str)
    human_message = HumanMessagePromptTemplate.from_template(user_str)
    chat_prompt = ChatPromptTemplate.from_messages([system_message, human_message])
    
    # 4. Partial Binding (偏函数绑定)
    global PROMPT_PIPELINE
    PROMPT_PIPELINE = chat_prompt.partial(examples=examples_str)
    
    return True, "系统初始化成功"

这一步先将系统提示词模板和用户提示词模板合并,再刚才拼接好的范文传入系统提示词模板中,此时模板中只剩下{topic}, {event}, {requirements}三个占位符。

核心生成逻辑

def generate_article(model_repo, topic, event, requirements):
    # 0. 安全检查:确保 pipeline 已经初始化
    if PROMPT_PIPELINE is None:
        return "Error...", ...
    
    # 1. 获取 API Key (从环境变量获取,比硬编码安全)
    api_key = os.getenv("HF_TOKEN")

    # 2. 准备运行时参数
    runtime_params = {
        "topic": topic,
        "event": event,
        "requirements": requirements
    }

    try:
        # 3. 预览 Prompt (调试用)
        # 这一步是为了让用户在界面上看到:AI 到底接收到了什么指令
        final_prompt_value = PROMPT_PIPELINE.invoke(runtime_params)
        messages = final_prompt_value.to_messages()
        
        # 提取内容用于展示 (sys_display, user_display)
        # ... (截断过长的 System Prompt 以免撑爆 UI) ...

        # 4. 配置模型连接 (HuggingFaceEndpoint)
        endpoint = HuggingFaceEndpoint(
            repo_id=model_repo,               # 模型 ID (如 Qwen/Qwen2.5-72B-Instruct)
            huggingfacehub_api_token=api_key, # 鉴权 Key
            temperature=0.6,                  # 创造性 (0.6 比较均衡,太高会胡言乱语)
            max_new_tokens=4096,              # 最大输出长度 (足够写一篇长演讲)
            top_k=50
        )

        # 5. 包装为聊天模型 (ChatHuggingFace)
        # 这一步至关重要。它会自动把 System Message 和 Human Message 
        # 转换成模型能听懂的格式 (例如 Qwen 的 <|im_start|>)
        chat_model = ChatHuggingFace(llm=endpoint)

        # 6. 构造调用链 (Chain)
        # Prompt模板 -> Chat模型
        chain = PROMPT_PIPELINE | chat_model
        
        # 7. 执行调用
        response = chain.invoke(runtime_params)
        
        # 8. 返回结果
        # response 是一个 AIMessage 对象,我们需要它的 .content 属性(纯文本)
        return sys_display, user_display, response.content

    except Exception as e:
        # 错误处理:如果断网或 Key 错误,返回友好的提示
        return "...", "...", f"❌ 调用 AI 失败: {str(e)}..."

generate_article函数在每次用户点击生成按钮时调用,负责将通过参数传入的用户输入合并到提示词中,并向 Hugging Face 上的大语言模型发起请求。用户可以选择特定的模型。

这里的 Token 通过环境变量获取,部署到 Hugging Face Spaces 时要设置 secrets。

Gradio 界面实现

界面实现比较简单,分为左右两个部分,左侧用于用户输入,右侧用于展示结果,其中右侧通过标签页实现结果和调试信息的切换。

创建app.py文件

import gradio as gr
import utils

# 1. 应用启动时尝试初始化数据
success, msg = utils.init_pipeline()
print(f"Server Log: {msg}")

# 2. 定义 Gradio 界面
def run_app():
    with gr.Blocks(title="李立军模拟器 (Hugging Face版)", theme=gr.themes.Soft()) as demo:
        gr.Markdown("# 🏫 李立军风格演讲生成器")
        gr.Markdown(f"状态: *{msg}*")
        
        with gr.Row():
            # --- 左侧配置区 ---
            with gr.Column(scale=1):
                gr.Markdown("### 🛠️ 配置与输入")
                
                # 模型选择
                model_repo = gr.Dropdown(
                    label="选择模型",
                    choices=[
                        "Qwen/Qwen2.5-72B-Instruct", 
                        "Qwen/Qwen3-Next-80B-A3B-Instruct",
                        "Qwen/Qwen3-235B-A22B-Instruct-2507"
                    ],
                    value="Qwen/Qwen2.5-72B-Instruct",
                    interactive=True
                )

                gr.Markdown("---")
                
                input_topic = gr.Textbox(label="演讲主题", value="关于严禁在实验室玩原神")
                input_event = gr.Textbox(label="导火索事件", value="刚才有个后生做实验的时候在那抽卡", lines=2)
                input_req = gr.Textbox(label="具体要求", value="痛斥玩物丧志,结合阶层固化,结尾强调实验室纪律", lines=3)
                
                btn_submit = gr.Button("🚀 开始生成", variant="primary")

            # --- 右侧结果区 ---
            with gr.Column(scale=2):
                gr.Markdown("### 📝 生成结果")
                
                with gr.Tabs():
                    with gr.TabItem("AI 回复"):
                        output_ai = gr.Markdown(label="生成的文章", min_height=400)
                    
                    with gr.TabItem("调试信息"):
                        output_sys = gr.Textbox(label="System Prompt (含范文)", lines=5)
                        output_user = gr.Textbox(label="User Prompt (指令)", lines=3)

        # --- 事件绑定 ---
        btn_submit.click(
            fn=utils.generate_article, # 调用 utils 里的函数
            inputs=[ model_repo, input_topic, input_event, input_req],
            outputs=[output_sys, output_user, output_ai]
        )

    return demo

if __name__ == "__main__":
    app = run_app()
    app.launch()

这里提供了三个大语言模型供用户选择,选择之后填写所需的参数,就可以点击按钮,调用utils.py中的generate_article函数,获取生成的结果并展示给用户。

值得注意的是,右侧展示结果的文本框要设置一个最小高度,否则生成演讲稿时的加载动画会被遮挡,使用体验非常不好。

部署

我选择直接部署到 Hugging Face Spaces 上以便使用。这里需要在 Hugging Face 的 Settings 中创建一个 Access Token(权限需包含 write)用于仓库推送。同时,在 Spaces 的 Settings 页面添加 Repository Secret(变量名需与代码中的 HF_TOKEN 一致),以便程序在运行时安全获取 Token。注意分支名为main而不是master,推送到master分支的话 Hugging Face Spaces 页面上不会有任何变化。

李立军风格演讲生成器

小结

经过初次学习 LangChain 工程之后,我认为 LangChain 确实大幅降低了大语言模型的复杂度,对于当下流行的 AI 赋能工程来说有相当重要的地位,值得进一步深入学习。

本文以及本文内链接中的网页中出现的所有人名均为虚构,如有雷同,纯属巧合。