AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


关于上述代码的一些感受
发布日期:2024-05-02 18:23:05 浏览次数: 1849


作者:fanfan

邮箱:beingfanfan@gmail.com

文章原创,转载请联系。

目录

  • 安装相关Package

  • 文本准备:长篇小说HP4

  • 用Langchain切分文本

    • 方法一:Stuffing

    • 方法二:Map-Reduce

    • 方法三:Refine

  • 关于上述代码的一些感受


随着大语言模型的不断普及,我们已经可以经常使用各类模型对文本进行高质量的文本总结。然而,大部分大语言模型接口都会对输入文本的长度有所限制,这个限制通常被称为"上下文窗口(context window)"。当需要总结的文本长度超过这个窗口时,我们就无法直接将全文喂给模型,需要采取一些特殊的处理方式。

本文将介绍如何使用LangChain,结合OpenAI的GPT语言模型gpt-3.5-turbo-1106(这个模型的窗口长度限制为16385个tokens),实现对超出OpenAI API上下文窗口长度限制的长文本进行总结。

我们将以长篇小说"哈利波特与火焰杯"作为示例文本,探索两种长文本总结方法,并对比分析它们的优缺点。


安装相关Package

我们首先需要安装所需的Python包:

pip install --upgrade langchain openai tiktoken chromadb

安装完成后,在代码开头导入以下模块:

from langchain_openai import ChatOpenAI
from langchain.chains.summarize import load_summarize_chain
from langchain_community.document_loaders import WebBaseLoader
from langchain.chains.combine_documents.stuff import StuffDocumentsChain
from langchain.chains.llm import LLMChain
from langchain_core.prompts import PromptTemplate
from langchain.chains import MapReduceDocumentsChain, ReduceDocumentsChain
from langchain_text_splitters import CharacterTextSplitter
from langchain.docstore.document import Document
import tiktoken
import openai
import os
openai.api_key = os.getenv("OPENAI_API_KEY")

文本准备:长篇小说HP4

接下来,我们准备待总结的文本。本示例使用的是Harry Potter系列第四部"哈利波特与火焰杯"。由于OpenAI API调用需要付费,为了演示目的,我只截取了第31章之后的部分内容,包含45095个tokens。

这一部分刚好包含了哈利波特参加第三个火焰杯项目的内容,以及伏地魔复活的内容,算是一个比较完整连贯的情节。

# 读入harry potter 4的文本
with open("Harry Potter and the Goblet of Fire.txt"as f:
    text = f.read()
    text = text.replace("\n"" ")
text = text.split("Chapter 31")[1]

看看待总结的文本前300个字符:

The Third Task

“Dumbledore reckons You-Know-Who’s getting stronger again as well?” Ron whispered.
Everything Harry had seen in the Pensieve, nearly everything Dumbledore had told and shown him afterward, he had now shared with Ron and Hermione — and, of course, with Sirius, to whom Harry had sent 

我们用tiktoken来计算整个文档的token数量:

# 计算整个待总结文档的token数
def num_tokens_from_string(string: str, encoding_name: str) -> int:
    """Returns the number of tokens in a text string."""
    encoding = tiktoken.get_encoding(encoding_name)
    num_tokens = len(encoding.encode(string))
    return num_tokens

doc_tokens = num_tokens_from_string(text, "cl100k_base")
print(f"待总结文档共有 {doc_tokens} 个token")

输出如下:

待总结文档共有 45095 个token

用Langchain切分文本

由于GPT模型对输入的上下文长度有限制,无法一次性处理过长的文本。因此,我们首先需要先将长文本切分成多个较短的文本块。Langchain提供了多种文本切分器,可根据需求选择合适的切分方式。

RecursiveCharacterTextSplitter会先将输入文本按照指定的分隔符切分成粗糙的文本块,然后根据chunk_size参数将这些块进一步切分,使每个最终文本块的字符数不超过指定值chunk_size=10000 ,而model_name="gpt-3.5-turbo-1106" 指定使用gpt-3.5-turbo-1106模型的编码信息。

from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    model_name="gpt-3.5-turbo-1106",
    chunk_size=10000,
    chunk_overlap=20,
)

pages = text_splitter.split_text(text)
texts = [Document(page_content=p) for p in pages]
print(len(texts))

方法一:Stuffing

Stuffing是最简单直接的总结方法,即直接将全文内容一次性传递给语言模型,让模型直接基于全文生成摘要。

它的优点是只需调用API一次,速度较快;但缺点是受限于语言模型的最大上下文长度,当文本过长时无法处理全文(我们首先讲Stuffing方法,只因为它是下面两类长文本总结方法的一个对照组)。

因此,在下面的代码中,我们只传入了切分后的第一个文本块texts[0],之前我们在切分文本的函数中设置了chunk_size=10000,因此我们拿到的单个文本块不会超过GPT模型的上下文窗口限制。

# 写法1:使用 load_summarize_chain 
prompt_template = """Write a concise summary in chinese of the following:
"{text}"
CONCISE  SUMMARY:"""

prompt = PromptTemplate(template=prompt_template, input_variables=["text"])
llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo-1106")
chain = load_summarize_chain(llm, chain_type="stuff",prompt=prompt)
result = chain.run([Document(texts[0].page_content)])
result

输出如下:

'哈利和他的朋友们讨论了邓布利多的警告,认为伏地魔正在变得更加强大。他们还讨论了斯内普的信任问题,以及里塔·斯基特的报道。哈利和朋友们准备参加第三个任务,他们在迷宫中遇到了各种挑战,包括巨大的怪兽和谜题。在迷宫中,哈利遇到了克鲁姆,他使用了不可饶恕的咒语对待了其他选手。最后,哈利遇到了一个人面狮身的斯芬克斯,她提出了一个谜题,哈利成功回答后继续前进。'

我们也可以使用StuffDocumentsChain来实现文本总结的功能。写法2输出的写法1的输出是等价的。

# 写法2:使用 StuffDocumentsChain
prompt_template = """Write a concise summary in chinese of the following:
"{text}"
CONCISE CHINESE SUMMARY:"""

prompt = PromptTemplate.from_template(prompt_template)
llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo-1106")
llm_chain = LLMChain(llm=llm, prompt=prompt) 
chain = StuffDocumentsChain(llm_chain=llm_chain, document_variable_name="text")
result = chain.run([Document(texts[0].page_content)])
result

方法二:Map-Reduce

当要总结的文本长度超过模型的最大上下文长度时,Stuffing就无用了。Map-Reduce是一种更高级的文本摘要策略,核心思想是"分而治之"

  • 将长文本切分成多个小段落
  • 对每个小段落调用API生成摘要
  • 将各段落摘要合并,再次调用API生成最终的全文摘要

优点是可以总结任意长度的文本。缺点是需要多次调用API,速度较慢,成本较高。

我们可以使用LangChain实现Map-Reduce文本摘要:

# 写法1:使用 load_summarize_chain
prompt_template = """Write a concise summary in chinese of the following text:
"{text}"
CONCISE CHINESE SUMMARY:"""

prompt = PromptTemplate(template=prompt_template, input_variables=["text"])
llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo-1106")
chain = load_summarize_chain(llm, chain_type="map_reduce", map_prompt=prompt, combine_prompt=prompt,token_max = 10000)
result = chain.run(texts)

我们也可以使用MapReduceDocumentsChain

# 写法2,使用 MapReduceDocumentsChain,自定义map_prompt和reduce_prompt
# map chian
map_template = """
Write a summary in chinese of this chunk of text that includes the main points and any important details.
{texts}
"""

map_prompt = PromptTemplate.from_template(map_template)
map_chain = LLMChain(llm=llm, prompt=map_prompt)

# reduce chain
reduce_template = """The following is set of summaries in Chinese:
{texts}
Take these and distill it into a final, consolidated summary in chinese. 
CHINESE ANSWER:"""

reduce_prompt = PromptTemplate.from_template(reduce_template)
reduce_chain = LLMChain(llm=llm, prompt=reduce_prompt)

# combine chain
combine_documents_chain = StuffDocumentsChain(
    llm_chain=reduce_chain, document_variable_name="texts"
)

# reduce chain
reduce_documents_chain = ReduceDocumentsChain(
    combine_documents_chain=combine_documents_chain,
    collapse_documents_chain=combine_documents_chain,
    token_max = 10000,
)

# map-reduce chain
map_reduce_chain = MapReduceDocumentsChain(
    llm_chain=map_chain,
    reduce_documents_chain=reduce_documents_chain,
    document_variable_name="texts",
    return_intermediate_steps=True,
)

result = map_reduce_chain.invoke(texts)
print(result['output_text'])

输出如下:

哈利、罗恩和赫敏在准备第三项任务时,讨论了邓布利多对斯内普的信任、丽塔·斯基特的报道、以及对巴格曼和马德琳·马克西姆的猜测。他们在黑魔法防御课上练习咒语,准备迎接挑战。在任务当天,哈利和塞德里克一起抓住了三强杯,但被传送到了一个墓地。伏地魔出现,宣布要杀死哈利,展示自己的力量。任务结束后,哈利发现自己被绑在伏地魔父亲的墓碑上,面对着伏地魔的威胁。在最后一刻,哈利使用驱魔术成功反击了伏地魔,展现出了勇敢和决心。邓布利多在离别宴会上向全校师生宣布了塞德里克·迪戈里被伏地魔谋杀的消息,并表扬了哈利·波特的勇敢行为。他强调了团结的重要性,呼吁大家共同对抗伏地魔的黑暗势力。同时,海格和马德姆·马克西姆被派去执行一项秘密任务。整个学校都在为即将到来的黑暗和困难时期做准备。

方法三:Refine

Map-Reduce虽然可以处理任意长度的文本,但仍有改进空间。因为reduce阶段只是机械地将各段落摘要拼接,再做一次总结,并没有考虑上下文衔接、冗余去重等。

Refine是一种迭代式总结方法:

  • 将文本切分成多个部分
  • 总结第一部分得到摘要A
  • 将第二部分文本与摘要A拼接,再次总结得到摘要B
  • 将第三部分文本与摘要B拼接,再次总结得到摘要C
  • 依此类推,不断迭代,直到文本全部处理完
  • 最后一次总结的结果即为全文摘要

与Map-Reduce相比,Refine的特点是每一步迭代都会将上一步的中间摘要结果作为上下文,与当前文本片段一起输入,不断细化总结结果。LangChain对Refine方法也提供了支持:

prompt_template = """Write a concise summary of the following:
{text}
CONCISE SUMMARY:"""

prompt = PromptTemplate.from_template(prompt_template)


refine_template = (
    "Your job is to produce a final comprehensive summary in Chinese, considering all the context provided so far, including: {existing_answer}\\n"
    "We have the opportunity to further refine and build upon the existing summary"
    "with some more context below.\\n"
    "------------\\n"
    "{text}\\n"
    "------------\\n" 
    "Given the new context, refine and original summary comprehensively and concisely in Chinese, making sure to cover important details from the entire context."
)

refine_prompt = PromptTemplate.from_template(template=refine_template)
chain = load_summarize_chain(
    llm=llm,
    chain_type="refine",
    question_prompt=prompt,
    refine_prompt=refine_prompt,
    return_intermediate_steps=True,
    input_key="input_documents",
    output_key="output_text",
)
result = chain({"input_documents": texts}, return_only_outputs=True)

输出:

哈利、罗恩和赫敏讨论伏地魔的威胁不断增长,以及邓布利多对斯内普的信任。他们还为三巫魔杯比赛的第三项任务做准备,哈利练习咒语和法术。比赛当天,哈利和其他冠军进入了一个充满障碍和生物的迷宫。哈利遇到了一位摄魂怪、一只爆裂蠕虫和一只狮身人面兽,最终与克鲁姆对峙,克鲁姆对塞德里克使用了不可饶恕的咒语。哈利和塞德里克分道扬镳,哈利遇到了狮身人面兽,狮身人面兽用谜语向他挑战。在迷宫中,哈利和塞德里克一起触摸了三巫魔杯,结果被传送到了一个墓地。伏地魔重获人形,向死喷火,准备杀死哈利。在墓地上,哈利和伏地魔进行了一场激烈的对决,最终哈利成功利用三巫魔杯的力量逃脱,带着塞德里克的尸体回到霍格沃茨。接着,哈利被发现了一个隐藏的真相,原来一直以来的“麻瓜”魔法师是伪装的,实际上是巴蒂·克劳奇,他被伏地魔派来执行一系列阴谋。同时,邓布利多与斯内普和西里斯进行了重要的对话,以及与魔法部长福吉的交涉。整个故事充满了悲伤和挑战,但也展现了哈利的勇气和决心。最后,哈利在回到普里怀特大街之前,与弗雷德和乔治分别,将三巫魔杯的奖金交给了他们,以支持他们的玩笑商店。

关于上述代码的一些感受

在运行上述代码时,我感觉至少调整了几十次prompt的设计……map_reducerefine似乎都对prompt异常敏感,哪怕只是prompt微小的改动,最终结果也会有很大出入。

另外,refine似乎很容易遗忘最初的一些关键信息,尤其是在概括小说情节时,它好像会忽视前期文本总结,而过于侧重后续的文本。再加上我一直要求模型从英文原文中提炼出中文摘要,在翻译过程中也会逐步累积一些小错误,导致经过多次迭代后,最终生成的结果显得有点奇怪。

总之,我想说的是,对于这类任务,根据具体文本内容对prompt进行定制化设计是至关重要的,尤其是在使用map_reduce和refine这种需要编写多个prompt的情况下。而上述代码只是提供了一种可能的思路,直接照搬可能会存在一些问题,需要自己多炼炼。

此外,LangChain官方文档对于使用load_summarize_chain(写法1)和直接加载MapReduceDocumentsChainReduceDocumentsChainRefineDocumentsChain(写法2)的区别解释得不够清晰,给出的示例代码运行起来也存在一些问题。我个人猜测可能直接加载load_summarize_chain有内置的prompt,比较快速,而加载不同的链则侧重于更多自定义的细节。

最后,五一劳动节快乐,祝各位劳动节不劳动。



53AI,企业落地应用大模型首选服务商

产品:大模型应用平台+智能体定制开发+落地咨询服务

承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

与创始人交个朋友

回到顶部

 
扫码咨询