LangChain入门到实战:7-[高级]基于LCEL链式表达式语言构建复杂RAG问答

1. LCEL基本概念入门

LCEL(LangChain Expression Language)是 LangChain 中非常关键的概念,它指的是一种声明式的表达式语言,用于通过链式组合不同的模块。它将不同的组件通过统一的Runable接口连接起来,从而实现具体的流程或功能(比如RAG、Agent)。

因为每个模块都是LCEL对象,因此基于LCEL对象连接形成的链,本身也是一个 LCEL 对象,所以当通过一组通用的调用方法(invoke、 batch、stream、 ainvoke )方法时,就能够做到定制化组合链、并行化组件、回退、动态配置链内部结构等标准化的操作。它的优势非常明显:

    1. 统一的接口:每个 LCEL 对象都实现Runnable接口,因此可以非常方便的连接到一起;
    1. 模块化操作:每个组件都可以独立开发和测试,处理好输入和输出就可以通过LCEL集成到一起;
    1. 良好扩展性:各个模块组件之间都实现了通用的调用方法(invoke、 batch、stream、 ainvoke )方法,因此可以灵活组合使用不同的使用场景;
        比如LangChain中抽象出来的最简单的 Model I/O 模块。

LangChain的Model I/O模块提供了标准的、可扩展的接口实现与大语言模型的外部集成。所谓的Model I/O,包括模型输入(Prompts)、模型输出(OutPuts)和模型本身(Models),简单理解就是通过该模块,我们可以快速与某个大模型进行对话交互,整个内部逻辑就相当于我们最熟悉的这个过程:输入Prompt,得到大模型针对该Prompt的推理结果。如下示例为OpenAI的 GPT 系列模型的API 调用规范:

import os
from dotenv import load_dotenv
load_dotenv()
True

response = openai.ChatCompletion.create(
  model="gpt-3.5-turbo",
  messages=[
    {"role": "user", "content": "请问,什么是机器学习?"}
  ]
)

在LangChain的Model I/O模块设计中,包含三个核心部分: Prompt Template(对应上图中的Format部分), Model(对应上图中的Predict部分) 和Output Parser(对应上图中的Parse部分)。

  • Format:即指代Prompts Template,通过模板化来管理大模型的输入;
  • Predict:即指代Models,使用通用接口调用不同的大语言模型;
  • Parse:即指代Output部分,用来从模型的推理中提取信息,并按照预先设定好的模版来规范化输出。
  • Format

对于Prompt Template第一部分,传统上我们创建提示词是通过手工编写来实现的,在这个过程中会利用各种提示工程技巧,如Few-Shot、链式推理(CoT)等方法,以提高大模型的推理性能。然而,在应用开发中,一个关键的考量是提示词不能是一成不变的。 其原因在于,应用开发需要适应多变的用户需求和场景。固定的提示词限制了模型的灵活性和适用范围。例如,如果我们正在开发一个天气查询应用,用户可能会以多种方式提出查询,如“今天的天气怎么样?”或“明天纽约的温度是多少度?”。如果提示词是固定的,它可能只能处理一种特定类型的查询,而无法适应这种多样性的需求。

而Prompt Template,就像ReAct模版,将API的使用、问题解答过程等复杂逻辑封装成了一套结构化的格式。我们只需准备具体的外部函数信息和用户查询,即可生成定制化的提示词,引导模型按照既定逻辑进行思考和回答,从而实现外部函数的调用过程,即:

# 将一个插件的关键信息拼接成一段文本的模版。
TOOL_DESC = """{name_for_model}:调用此工具与 {name_for_human} API 交互。{name_for_human} API 有何用途?{description_for_model} 参数:{parameters}"""

# ReAct prompting 的 instruction 模版,将包含插件的详细信息。
PROMPT_REACT = """请尽可能完整地回答以下问题。您可以使用以下 API:

{tool_descs}

请使用以下格式:

问题:您必须回答的输入问题
思考:您应该始终思考要做什么
行动:要采取的行动,应为 [{tool_names}] 之一
行动输入:行动的输入
观察:行动的结果
……(此思考/行动/行动输入/观察可以重复零次或多次)
思考:我现在知道最终答案了
最终答案:原始输入问题的最终答案

开始!

问题:{query}"""

因此,引入Prompt Template可以支持变量和动态内容的插入,使得同一个应用可以根据不同的输入动态调整提示词,从而更好地响应用户的具体需求。LangChain通过这种方式来提高应用的通用性和用户体验。

  • Predict

    在Predict部分,实质上是处理模型从接收输入到执行推理的整个过程。考虑到存在两种主要类型的大模型——Base类模型和Chat类模型,LangChain在其Model I/O模块中对这两种模型都进行了抽象,分别归类为LLMs(Large Language Models)和Chat Models。我们还是以OpenAI 的 Completion 和 Chatcompletions方法为例:


# Base类模型
client.completions.create(
  model="gpt-3.5-turbo-instruct",
  prompt="这是一个测试",
)


# 聊天模型
client.chat.completions.create(
  model="gpt-3.5-turbo",
  messages=[
    {"role": "system", "content": "你是一位乐于助人的AI智能小助手"},
    {"role": "user", "content": "你好,请你介绍一下你自己。"}
  ]
)

LLMs是简化的大语言模型抽象,即基于给定的Prompt提供内容生成的功能。而Chat Models则专注于聊天API的抽象,需要维护上下文的记忆(聊天记录),呈现出更接近对话或聊天形式的交互。

  • Parse

我们知道,大模型的输出是不稳定的,同样的输入Prompt往往会得到不同形式的输出。在自然语言交互中,不同的语言表达方式通常不会造成理解上的障碍。但在应用开发中,大模型的输出可能是下一步逻辑处理的关键输入。因此,在这种情况下,规范化输出是必须要做的任务,以确保应用能够顺利进行后续的逻辑处理。

输出解析器 Output Parser就是一个帮助结构化语言模型响应的抽象,可以获取格式指令或者进行更深层次的解析。这我们会在后面的实践中直观的体验到。

整体而言,在Model I/O模块的抽象中,其一能够让开发者快速的接入不同的大模型,比如OpenAI、ChatGLM、Qwen等,按照既定规范执行模型推理。其二通过输入和输出的模板化处理,使其更贴合于应用开发的最佳实践。接下来,我们就逐步的介绍上述三个流程在LangChain下是如何进行集成和操作的。

# 不带 LCEL

from typing import List
import openai

prompt_template = "给我讲一个关于{topic}的小笑话"

client = openai.OpenAI(
    api_key = os.getenv("DEEPSEEK_KEY"),  # 如果是本地服务,有时可以随意填写或留空
    base_url = "https://api.deepseek.com"
)

def call_chat_model(messages: List[dict]) -> str:
    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=messages
    )
    return response.choices[0].message.content

def invoke_chain(topic: str) -> str:
    prompt_value = prompt_template.format(topic=topic)
    messages = [
        {
            "role": "user",
            "content": prompt_value
        }
    ]
    return call_chat_model(messages)

# Example call
print(invoke_chain("冰激凌"))
好的!这里有一个关于冰激凌的可爱小笑话:

---

**顾客**(举着融化的冰激凌):"服务员,我的冰激凌怎么这么快就化了?"  
**服务员**(淡定地):"因为它听说您要‘热’情款待它呀!"

(或者另一个版本:  
**服务员**:"因为它想给您表演一个‘消失的魔术’!")

---

希望这个甜甜的小笑话能让你嘴角上扬!😄🍦
基本示例:提示 + 模型 + 输出解析器

prompt + model + output parser

代码中的这一行,使用 LCEL 将这些不同的组件组合成一个链:

chain = prompt | model | output_parser

# LCEL 方式

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

# 定义提示模板
prompt = ChatPromptTemplate.from_template("给我讲一个关于{topic}的小笑话")

# 初始化模型
# model = ChatOpenAI(model="gpt-4o-mini")

model = ChatOpenAI(model="deepseek-chat",
                   api_key= os.getenv("DEEPSEEK_KEY"),
                   base_url='https://api.deepseek.com')

# 初始化输出解析器
output_parser = StrOutputParser()

# 构建链式调用(管道式执行,管道符号 | )
chain = (
    {"topic": RunnablePassthrough()} 
    | prompt 
    | model 
    | output_parser
)

# 调用链并传入输入
result = chain.invoke("冰淇淋")
print(result)
当然!这里有一个关于冰淇淋的小笑话:

**顾客**:老板,你们这儿的冰淇淋怎么这么贵啊?  
**老板**:因为我们的冰淇淋会“融化”你的心啊!  
**顾客**:那为什么还收我钱?  
**老板**:因为“融化”归融化,生意归生意嘛!  

(或者另一个版本:)  
**小孩**:妈妈,为什么冰淇淋是甜的?  
**妈妈**:因为它想让你的心情也变甜呀!  
**小孩**:那它为什么还会化掉?  
**妈妈**:……因为它看到你的作业本,被“热”哭了!  

希望你喜欢!😄🍦

流式输出

for chunk in chain.stream("小学生的"):
    print(chunk, end="", flush=True)
好的!这里有一个关于小学生的经典小笑话:

---

**老师问**:“小明,如果给你五块糖,再给你三块糖,你一共有几块糖?”  
**小明**:“不知道,我们数学课只教过苹果和香蕉!”  

**老师无奈**:“那换成苹果吧。给你五个苹果,再给你三个苹果,一共几个?”  
**小明**:“八个!”  

**老师欣慰**:“很好!那现在换成糖,五块加三块等于?”  
**小明**:“……老师,您能先把糖拿出来吗?”  

---

(笑点:小学生对抽象数学的“务实”理解,以及最后对糖果的执着 😄)

2、实现复杂RAG聊天机器人

接下来,我们进一步探讨 LangChain 和 DeepSeek v3模型如何构建一个复杂的 RAG 聊天机器人,能够处理复杂的查询,并且可以通过聊天历史记录维护上下文,并使用 LangChain 的 LCEL语法遵守严格的Guardrails(护栏)。

Guardrails(护栏)对于确保AI系统的安全性和可靠性是比较重要的。通过设定明确的界限,我们可以防止大模型生成有害或误导性的内容。拒绝机制使机器人能够礼貌地拒绝违反这些护栏的请求,例如与敏感主题或非法活动相关的请求。

这里我们创建一个智能HR聊天机器人助手,该机器人将能够利用私有知识库回答有关公司政策、程序和福利的问题。

from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

# 初始化大模型
model = ChatOpenAI(model="deepseek-chat",
                   api_key= os.getenv("DEEPSEEK_KEY"),
                   base_url='https://api.deepseek.com')

# 初始化Embedding模型
embed = OpenAIEmbeddings(model="text-embedding-3-large")

接下来,我们使用FAISS作为矢量数据库。 FAISS 是 Facebook AI Research 开发的一个库,用于高效相似性搜索和密集向量聚类。LangChain在第三方集成模块(Langchain_community)中已经接入了FAISS向量数据库,所以我们就可以直接使用。

from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document


# 加载一些模拟的假数据
doc1 = Document(page_content="员工每年享有一定数量的病假。有关资格和具体细节可以在员工手册中找到。")
doc2 = Document(page_content="员工请病假时,必须首先通知其主管关于病情和预计缺勤时间。员工需填写病假申请表,并提交给人力资源部门或主管。")
doc3 = Document(page_content="病假申请表可以在公司内部网找到。表格需要填写员工姓名、部门、缺勤日期和缺勤原因等信息。")


# 创建 Faiss 向量存储
vector_store = FAISS.from_documents([doc1, doc2, doc3], embed)

# 将文件保存到本地,包括:向量数据、索引文件和元数据文件
vector_store.save_local(folder_path='.')

创建矢量数据库后,我们可以进行测试:

# 加载本地的Faiss向量文件,allow_dangerous_deserialization 用于控制是否允许在加载向量存储时进行潜在的危险反序列化操作。
vector_store = FAISS.load_local(embeddings=embed, folder_path='.',allow_dangerous_deserialization=True)
 
# 将 FAISS 向量存储转换为一个 retriever(检索器),并为该检索器设置一些搜索相关的参数。k=1 表示检索时返回 最相似的 1 个文档
retriever = vector_store.as_retriever(search_kwargs={'k': 1})

# 执行相似度搜素
query = "请问我们公司有没有病假?"
results = retriever.invoke(query)

for doc in results:
    print(f"Content: {doc.page_content}")
Content: 员工每年享有一定数量的病假。有关资格和具体细节可以在员工手册中找到。

我们从一个最简单的链开始,只接受用户问题,在提示中格式化它并输出该问题的答案(不检索)。这里使用 Langchain 的PromptTemplate并使用LCEL对其进行管道传输。

from langchain.prompts import PromptTemplate
from langchain.schema.output_parser import StrOutputParser

# 定义提示模板
prompt = PromptTemplate(
  input_variables = ["question"],
  template = "你是一个乐于助人的智能小助理。擅长根据用户输入的问题给出一个简短的回答:: {question}"
)


# 构建Chains
chain = (
  prompt
  | model
  | StrOutputParser()
)
print(chain.invoke({"question": "请问什么是人工智能?"}))
人工智能(AI)是计算机科学的一个分支,旨在让机器模拟人类智能,包括学习、推理、问题解决和决策等能力。它通过算法和数据分析实现自动化任务,广泛应用于语音识别、图像处理、自动驾驶等领域。

在这个过程中,会将带有question键的字典被传递到提示模板中,其中question值被提取并在模板中格式化,然后作为输入传递到model,最后将结果提取为使用StrOutputPaser()最终输出字符串。

接下来,因为最终我们想要构建一个聊天机器人,所以需要让它支持聊天历史记录,作为RAG系统的一个基础组件。当调用链时,以列表的形式传递历史记录,指定每条消息是由用户还是助手发送的。例如:

{"role": "user", "content": "我每年可以请多少天病假?"}{"role": "assistant", "content": "你每年可以请的病假天数取决于你的具体雇佣合同和公司政策。然而,一般来说,员工有权享受一定的病假。具体细节请参阅员工手册或与人力资源部门联系。"},
   {"role": "user", "content": "我在哪里可以找到员工手册?"}{"role": "assistant", "content“: ”员工手册通常在公司内部网上提供。你也可以联系你的人力资源部门索要一份实体副本。"}

然后创建链组件,将此输入转换为传递给prompt_with_history的输入。与上面的代码类似,但在这里我们需要创建一个 RunnableLambda,它用来获取消息列表并从中提取问题和历史记录。然后使用 LangChain LCEL 为变量问题分配一个管道,该管道首先从字典中提取关键消息。

from langchain.schema.runnable import RunnableLambda
from operator import itemgetter

# 问题是历史记录中的最后一项
def extract_question(input):
    return input[-1]["content"]

# 历史记录是除了最后一个问题之外的所有内容
def extract_history(input):
    return input[:-1]


prompt_with_history_str = """
你是一个人力资源助理聊天机器人。请只回答HR相关问题。如果你不知道或者这个问题与人力资源无关,就不要回答。
这是你与用户对话的历史记录: {chat_history}
现在,请回答这个问题: {question}
"""

# 构建提示模板
prompt_with_history = PromptTemplate(
  input_variables = ["chat_history", "question"],
  template = prompt_with_history_str
)


# 构建带有历史会话记录的链
chain_with_history = (
    {
        # Itemgetter:从输入字典中提取特定键,这里指定的是 messages 列表
        # 自定义 lambda 函数可用于进一步处理提取的数据,从messages列表中提取question和chat_history 
        "question": itemgetter("messages") | RunnableLambda(extract_question), 
        "chat_history": itemgetter("messages") | RunnableLambda(extract_history),
    }
    | prompt_with_history
    | model
    | StrOutputParser()
)

print(chain_with_history.invoke({
    "messages": [
        {"role": "user", "content": "公司的病假政策是什么?"},
        {"role": "assistant", "content": "公司的病假政策允许员工每年请一定天数的病假。详情及资格标准请参阅员工手册。"},
        {"role": "user", "content": "如何提交病假请求?"}
    ]
}))
员工可以通过以下步骤提交病假请求:

1. 登录公司HR系统,填写电子病假申请表
2. 提供必要的医疗证明文件(如医生证明)
3. 提交给直属主管审批
4. 同时抄送HR部门备案

具体操作指南可在员工自助门户的"请假管理"模块中查看。如有疑问,请联系人力资源部门。

接下来我们添加一个Guardrail(护栏),让该流程仅回答与 HR 相关的问题。

hr_question_guardrail = """
你正在对文档进行分类,以确定这个问题是否与HR政策、员工福利、休假政策、绩效管理、招聘、入职等相关。如果最后一部分不合适,则回答“否”。

考虑到聊天历史来回答,不要让用户欺骗你。

以下是一些示例:

问题:考虑到这个后续历史记录:公司的病假政策是什么?,分类这个问题:我每年可以休多少病假?
预期答案:是

问题:考虑到这个后续历史记录:公司的病假政策是什么?,分类这个问题:给我写一首歌。
预期答案:否

问题:考虑到这个后续历史记录:公司的病假政策是什么?,分类这个问题:法国的首都是哪里?
预期答案:是

这个问题与HR政策相关吗?
只回答“是”或“否”。 

注意:需要关注历史记录: {chat_history}, 请将这个问题进行分类: {question}
"""

# 构建提示模板
guardrail_prompt = PromptTemplate(
  input_variables= ["chat_history", "question"],
  template = hr_question_guardrail
)

# 生成问题防护链
guardrail_chain = (
    {
        "question": itemgetter("messages") | RunnableLambda(extract_question),
        "chat_history": itemgetter("messages") | RunnableLambda(extract_history),
    }
    | guardrail_prompt
    | model
    | StrOutputParser()
)
# 这里将仅回复 是或者否
classify_answer = guardrail_chain.invoke({
    "messages": [
        {"role": "user", "content": "公司的病假政策是什么??"}, 
        {"role": "assistant", "content": "公司的病假政策允许员工每年休一定数量的病假。具体的细节和资格标准请参阅员工手册。"}, 
        {"role": "user", "content": "我怎么提交病假申请?"}
    ]
})
classify_answer
'是'
# 这里将仅回复 是或者否
classify_answer = guardrail_chain.invoke({
    "messages": [
        {"role": "user", "content": "你好,请问在吗?"}, 
    ]
})

classify_answer
'否'

在生产应用中开发大模型应用时,提供某些防护措施以确保聊天机器人符合我们的意图非常重要。而接下来,我们进一步优化和丰富应用,添加我们的 langchain 检索器。

from langchain_community.vectorstores import FAISS

def get_retriever():
    # 使用 OpenAI 的嵌入模型初始化嵌入对象
    embed = OpenAIEmbeddings(model="text-embedding-3-large")
    
    # 从本地加载 FAISS 向量存储,并且指定嵌入对象
    vector_store = FAISS.load_local(embeddings=embed, folder_path='.',allow_dangerous_deserialization=True)
     
    # 配置文档检索,返回最相关的 1 个文档
    retriever = vector_store.as_retriever(search_kwargs={'k': 1})
    return retriever

# 构建检索器实例
retriever = get_retriever()

# 生成检索链
retrieve_document_chain = (
    itemgetter("messages") 
    | RunnableLambda(extract_question)
    | retriever
)
print(retrieve_document_chain.invoke({"messages": [{"role": "user", "content": "病假的HR政策是什么?"}]}))
[Document(id='dc5ddca1-9eb0-4b9e-a170-21151d1a4cc3', metadata={}, page_content='员工每年享有一定数量的病假。有关资格和具体细节可以在员工手册中找到。')]

最后,我们实现完整的链来连接检索器。

from langchain.schema.runnable import RunnableBranch, RunnablePassthrough

# 定义 Prompt 模板字符串,用于生成最终回答提示
question_with_history_and_context_str = """
你是一个可信赖的 HR 政策助手。你将回答有关员工福利、休假政策、绩效管理、招聘、入职以及其他与 HR 相关的话题。如果你不知道问题的答案,你会诚实地说你不知道。
阅读讨论以获取之前对话的上下文。在聊天讨论中,你被称为“系统”,用户被称为“用户”。

历史记录: {chat_history}

以下是一些可能帮助你回答问题的上下文: {context}

请直接回答,不要重复问题,不要以“问题的答案是”之类的开头,不要在答案前加上“AI”,不要说“这是答案”,不要提及上下文或问题。

根据这个历史和上下文,回答这个问题: {question}
"""

# 使用 LangChain 的 PromptTemplate 封装上述提示模板,定义需要传入的变量
question_with_history_and_context_prompt = PromptTemplate(
  input_variables=["chat_history", "context", "question"],
  template=question_with_history_and_context_str
)

# 定义格式化检索文档的函数,将所有检索到的页面内容拼接成一个字符串
def format_context(docs):
    return "\n\n".join([d.page_content for d in docs])

# --- 分支链条逻辑 ---
# 链路1、定义不相关的链
# 如果问题与 HR 无关,则走这个链条:固定返回提示信息
irrelevant_question_chain = (
  RunnableLambda(lambda x: {"result": '我不能回答与 HR 政策无关的问题。'})
)

# 链路2、定义相关的链
# 如果问题与 HR 相关,走以下链条:
relevant_question_chain = (
  # 1. RunnablePassthrough:将输入数据原样传递
  # RunnablePassthrough 是 LangChain 中最基础的一个 可组合链条(Runnable),它的作用很简单——原样将输入传递到输出,不做任何处理。
  RunnablePassthrough() 
  |
  # 2. 生成查询并调用 RAG 检索链,提取历史记录和问题,同时从 retriever 检索文档
  {
    "relevant_docs": prompt | model | StrOutputParser() | retriever,
    "chat_history": itemgetter("chat_history"),
    "question": itemgetter("question")
  }
  |
  # 3. 将检索结果格式化为 context,构造完整提示模板所需输入
  {
    "context": itemgetter("relevant_docs") | RunnableLambda(format_context),
    "chat_history": itemgetter("chat_history"),
    "question": itemgetter("question")
  }
  |
  # 4. 构造 Prompt 对象,准备发送给 LLM 模型
  {
    "prompt": question_with_history_and_context_prompt,
  }
  |
  # 5. 执行 LLM 调用生成最终回答
  {
    "result": itemgetter("prompt") | model | StrOutputParser(),
  }
)

# --- 创建分支节点,根据是否相关进行流程控制 ---
branch_node = RunnableBranch(
  (lambda x: "是" in x["question_is_relevant"].lower(), relevant_question_chain),  # 问题相关,走相关链
  (lambda x: "否" in x["question_is_relevant"].lower(), irrelevant_question_chain),  # 问题不相关
  irrelevant_question_chain  # 默认 fallback
)

# --- 构造完整的链式流程 ---

full_chain = (
  {
    # 1. 判断问题是否相关,调用 guardrail_chain 得到 "是" / "否"
    "question_is_relevant": guardrail_chain,

    # 2. 提取用户提问内容
    "question": itemgetter("messages") | RunnableLambda(extract_question),

    # 3. 提取历史对话内容(可用于上下文补充)
    "chat_history": itemgetter("messages") | RunnableLambda(extract_history),
  }
  | branch_node  # 根据 relevance 进入不同链条
)
import json

non_relevant_dialog = {
    "messages": [
        {"role": "user", "content": "公司的病假政策是什么?"},
        {"role": "assistant", "content": "公司的病假政策允许员工每年休一定数量的病假。具体的细节和资格标准请参阅员工手册。"},
        {"role": "user", "content": "你好,请你介绍一下你自己呀。"}
    ]
}
print(f'用不相关的问题测试')
response = full_chain.invoke(non_relevant_dialog)
用不相关的问题测试
response
{'result': '我不能回答与 HR 政策无关的问题。'}
dialog = {
    "messages": [
        {"role": "user", "content": "公司的病假政策是什么?"},
        {"role": "assistant", "content": "公司的病假政策允许员工每年休一定数量的病假。具体的细节和资格标准请参阅员工手册。"},
        {"role": "user", "content": "我应该如何提交病假的申请?"}
    ]
}
print(retrieve_document_chain.invoke({"messages": [{"role": "user", "content": "我应该如何提交病假的申请??"}]}))
[Document(id='34cce586-cc17-4673-bb9e-1da1730a3704', metadata={}, page_content='员工请病假时,必须首先通知其主管关于病情和预计缺勤时间。员工需填写病假申请表,并提交给人力资源部门或主管。')]
print(f'用相关的问题测试')
response = full_chain.invoke(dialog)
用相关的问题测试
response
{'result': '通知主管病情和预计缺勤时间,填写病假申请表并提交给人力资源部门或主管。'}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值