LangChain,构建带有Milvus存储的问题分析任务

LangChain简述

LangChain 就是一个 LLM 编程框架,你想开发一个基于 LLM 应用,需要什么组件它都有,直接使用就行;甚至针对常规的应用流程,它利用链(LangChain中Chain的由来)这个概念已经内置标准化方案了。下面我们从新兴的大语言模型(LLM)技术栈的角度来看看为何它的理念这么受欢迎。

参考文档:
一文全面搞懂LangChain

PyMilvus简述

Milvus在我之前的文章中,已经有提到,并且讲述了如何使用Milvus Java SDK来构建存储,但在更广泛的LLM应用的场景中,Python是更为流行的开发语言,因此本文章为了能够快速构建一个基于LLM的分析任务,选择使用Python脚本。

任务目标

用户输入一个问题和相关的统计数据,并且提供了额外的一些文章/文档(PDF, TXT, DOC等),希望LLM能够结合这些信息来回答用户的问题,并给出合理地建议。

数据预处理

对文章切片,并进行向量化存入Milvus。

python 复制代码
    # 将文件内容,切分成N个块,每个块500个字符,同时连续两个段落之前会重复50字符
    def load_and_split_text_file(self, file_path, chunk_size=500, chunk_overlap=50):
        file_abs_path = os.path.expanduser(file_path)
        filename = os.path.basename(file_abs_path)
        loader = TextLoader(file_abs_path)
        documents = loader.load()
        text_splitter = CharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
        docs = text_splitter.split_documents(documents)
        segments = []
        for doc in docs:
            if len(doc.page_content) <= 0:
                continue
            info = {
                "filename": filename,
                "text": str(doc.page_content)
            }
            segments.append(info)
        return segments

    def insert_info_data(self, infos):
        """ 将数据插入到Milvus中。 """
        if len(infos) <= 0:
            print("empty infos, return")
            return
        # 构建列式数据集
        entities = [
            {
                "name": "filename",
                "type": DataType.VARCHAR,
                "values": [info["filename"] for info in infos]
            },
            {
                "name": "title",
                "type": DataType.VARCHAR,
                "values": [info["title"] for info in infos]
            },
            {
                "name": "embeddings",
                "type": DataType.FLOAT_VECTOR,
                # 使用AzureOpenAI,对每一个文章分段,进行向量化
                "values": [self.embed_by_azureopenai(info["text"]) for info in infos]
            },
            {
                "name": "text",
                "type": DataType.VARCHAR,
                "values": [info["text"] for info in infos]
            }
        ]
        # 插入数据到Milvus
        ids = self.insert_data(entities)
        # 持久化
        self._milvus.flush([self.collection_name])

insert_info_data(load_and_split_text_file('test_document.txt'))

相关文章内容检索

python 复制代码
def get_formatted_documents(question: str, document_names: Optional[list[str]]) -> str:
    """ 格式化根据相似性搜索得到的文章内容 """
    content = transform_documents_to_str(search_documents(question, document_names))
    tokens_num = _estimate_tokens_num(content)
    if tokens_num > 3000:
        # TODO: using map/reduce to generate the summary
        #raise RuntimeError(f"Too many tokens {tokens_num} encoded from docs by model {models.model_factory.CHAT_MODEL_DEFAULT.model_name}, hence we will try to output the summary based on your question!")
        agent = initialize_agent(
            llm=models.model_factory.get_chat_model(temperature=0),
            tools=tools.copilot_tools.get_tools(),
            agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION,
            verbose=True,
            max_tokens=1000,
            handle_parsing_errors=True)
        return agent.invoke({
            "input": "Summary the documents according to the question:\n\n# question\n{}\n# content\n{}".format(question, "\n".join(documents))
        })['output']
    return content

def search_documents(question: str, document_names: Optional[list[str]]) -> list[dict]:
    """ 根据相似性搜索,查找与question相关的文章内容 """
    content = []
    try:
        if document_names is not None and len(document_names) > 0:
            datas = []
            for document_name in document_names:
                results = milvus_utility.search_by_titles(question, [document_name])
                datas.extend(results)
            print("data len:{}".format(len(datas)))
            return datas
    except BaseException:
        print(sys.exc_info())
    if len(content) > 0:
        print("!!!!!!!!!!!search result valid!!!!!!!!!!!!!!")
    return content

基于Chain构建工作流

关于Chain,这里一共定义了两大类Chain:

一类是summary_chain,负责根据问题、数据、相关文章内容,生成一个总结性的报告;一类是output chain,如publish_chain,负责对cummary_chain的输出结果再加工,然后转发给客户端。

关于Cache,每一个Chain就是与LLM的一次交互,很容易想到的一个问题是,对于相同的输出内容,实际上它的输出结果应该也是相同的,因此这里需要对每一个Chain进行缓存的判断,但由于是样例代码Cache实现地比较粗糙,仅仅基于threading.Lock{},故更好的做法是引入第三方的缓存框架。

python 复制代码
def summary_by_chain(questionAndData: list, document_names: Optional[list[str]], **kwargs):
    # 生成用于描述问题、统计数据以及相关的文章内容的Prompt
    question, prompt = PromptGenerator.generate_prompt_for_chain(
        questionAndData, document_names, lambda text, docs: documents_helper.get_formatted_documents(text, docs))
    # 构建一个回答用户question的工作链
    summary_chain = utils.chain_util.debug_wrapper(_construct_chain_with_one_prompt(prompt, question=question, **kwargs), **kwargs)

    outputs_chain = RunnableParallel(
        # 这个字段缓存了summary_chain的结果
        summaries=lambda x: x['input'],
        # append a publishing chain to the summary chain
        publish_output=lambda x: _generate_publish_output_with_cache(x['input'], **kwargs),
        # other chain need input
        other_output=lambda x: _generate_output_with_cache(x['input'], **kwargs),
     )

    # outputs chain's input comes from summary chain
    # outputs_chain的输入变量,来自于summary_chain的输出
    outputs = outputs_chain.invoke({'input': _generate_summary_with_cache(questionAndData, documents, summary_chain)})

    output = {
        'documents': document_names,
        'referenced_documents': document_names,
        'summary': outputs['summaries'],
        'dynamics': {
            'publish_content': outputs['publish_output'],
            'other_output': outputs['other_output']
        }
    }

    print("Output from AI: " + str(output))
    return output

def _construct_chain_with_one_prompt(prompt: str, **kwargs):
    # 可以提供语言,以使得LLM按指定的语言输出结果
    summary_chain = (
            {'language': lambda x: kwargs['language']}
            | ChatPromptTemplate.from_template(prompt)
            | model_factory.CHAT_MODEL_DEFAULT
            | StrOutputParser())
    final_chain = summary_chain.with_config(tags=['summary_chain'])
    return final_chain

def _generate_summary_with_cache(questionAndData, documents, chain, **kwargs):
    # 根据问题及数据,找到是否存在缓存,如果有,则直接返回之前已经生成的output,
    # 否则才需要执行chain
    cached_summary = GLOBAL_CACHE.load(questionAndData=questionAndData, documents=documents, cache_type="summaries")
    return cached_summary if cached_summary is not None else GLOBAL_CACHE.save(questionAndData, documents, lambda x: chain.invoke(""), cache_type="summaries")


def _generate_publish_output_with_cache(content, **kwargs):
    # 对content进行加密,以此作为识别不同content的依据,利用Cache逻辑,
    # 避免不必要的Chat
    md5 = hashlib.md5(content.encode('utf-8'))
    cache_key = "PUBLISH:" + md5.hexdigest()
    return _generate_output_with_cache(cache_key, content, lambda x: PublishTool.get_publish_chain(**kwargs).invoke({'summaries': x}))

方案改进

文章内容检索

由于内容检索的好坏,会由如下几点影响:

  1. 文章切分好坏程度
  2. 文章相关性及多相关性

针对影响1:可以考虑使用更加格式化的文档格式,如Markdown, XML等,以避免chunk的切分方式,提升切分后的段落内聚性;

针对影响2 :考虑利用LLMChainExtractor工具,检索到相关文章内容后,通过绑定的LLM对原文章内容做精简,达到去冗余的目的;同时考虑结合MMR(Maximal Marginal Relevance)搜索算法,使返回的文章内容尽量相关且多样化,避免文章内容过于聚焦。

使用Agent模式取代Chain链模式

Chain很明显的一个问题是,我们不得不事先定义好工作链,并为每一个Agent构造合适的Prompt,而无法充分利用LLM模型对任务的拆解能力,理论上可以通过Agent模式,让LLM分析任务、自动调用function call / tool,最终完成整个任务流,以取代Chain的固定模式。

但Agent也有缺点,就是稳定性和时效性。所谓稳定性,是每一次都需要AI交换确认下一步的工作是什么,一旦Clues信息描述不清楚以及输入数据难以理解,很容易产生不符合预期的结果;对于时效性,下一步应该做什么都需要经过一次LLM交互,因此时间必须是远大于Chain链的固定模式。

当然也可以结合Agent和Chain的优点完成工作。

相关推荐
数据智能老司机1 小时前
精通 Python 设计模式——分布式系统模式
python·设计模式·架构
数据智能老司机2 小时前
精通 Python 设计模式——并发与异步模式
python·设计模式·编程语言
数据智能老司机2 小时前
精通 Python 设计模式——测试模式
python·设计模式·架构
数据智能老司机2 小时前
精通 Python 设计模式——性能模式
python·设计模式·架构
c8i2 小时前
drf初步梳理
python·django
每日AI新事件2 小时前
python的异步函数
python
玲小珑3 小时前
LangChain.js 完全开发手册(九)LangGraph 状态图与工作流编排
前端·langchain·ai编程
这里有鱼汤3 小时前
miniQMT下载历史行情数据太慢怎么办?一招提速10倍!
前端·python
databook12 小时前
Manim实现脉冲闪烁特效
后端·python·动效
程序设计实验室13 小时前
2025年了,在 Django 之外,Python Web 框架还能怎么选?
python