迭代搜索维基百科与 Claude

[免责声明:本笔记本使用 Claude 2 模型创建,现已视为旧版。]

有些问题 Claude 无法凭空回答。也许它们是关于时事的。也许你有一个 Claude 没有记住答案的极其详细的问题。没关系!通过一些提示和脚手架,Claude 可以搜索网络来找到答案。在本笔记本中,我们将创建一个虚拟研究助手,它能够搜索维基百科来找到你问题的答案。相同的方法也可用于允许 Claude 搜索更广泛的网络,或你提供的一组文档。

方法是什么?大体上它属于“工具使用”类别。我们创建一个搜索工具,告诉 Claude 关于它的信息,然后让它开始工作。用伪代码表示:

  1. 用搜索工具的描述、如何最好地使用它以及如何“调用”它(通过发出特殊字符串)来提示 Claude。
  2. 告诉 Claude 你要问的问题。
  3. Claude 像往常一样生成 token。如果它生成了特殊字符串,则终止 token 生成流,并向搜索 API 发出查询。
  4. 构建一个新提示,该提示由步骤 1 中的提示、Claude 生成的所有内容直到搜索调用字符串以及 API 调用结果组成。
  5. 重复此过程,直到 Claude 决定完成。

让我们深入研究用于工具使用和检索的提示。

提示

# 工具描述提示
wikipedia_prompt = """你将由一位人类用户提出问题。你可以使用以下工具来帮助回答问题。 <tool_description> 搜索引擎工具 * 搜索引擎将专门搜索与你的查询相似的维基百科页面。它为每个页面返回其标题和完整的页面内容。如果你想获取关于某个主题的最新和全面的信息来帮助回答查询,请使用此工具。查询应尽可能原子化——它们只需要解决用户问题的一部分。例如,如果用户的问题是“篮球的颜色是什么?”,你的搜索查询应该是“篮球”。再举一个例子:如果用户的问题是“谁创建了第一个神经网络?”,你的第一个查询应该是“神经网络”。正如你所见,这些查询非常简短。关键词优先,而非短语。 * 你可以随时使用以下语法调用搜索引擎:<search_query>查询词</search_query>。 * 然后你将在 <search_result> 标签中获得结果。</tool_description>"""
print(wikipedia_prompt)
你将由一位人类用户提出问题。你可以使用以下工具来帮助回答回答问题。 <tool_description> 搜索引擎工具 * 搜索引擎将专门搜索与你的查询相似的维基百科页面。它为每个页面返回其标题和完整的页面内容。如果你想获取关于某个主题的最新和全面的信息来帮助回答查询,请使用此工具。查询应尽可能原子化——它们只需要解决用户问题的一部分。例如,如果用户的问题是“篮球的颜色是什么?”,你的搜索查询应该是“篮球”。再举一个例子:如果用户的问题是“谁创建了第一个神经网络?”,你的第一个查询应该是“神经网络”。正如你所见,这些查询非常简短。关键词优先,而非短语。 * 你可以随时使用以下语法调用搜索引擎:<search_query>查询词</search_query>。 * 然后你将在 <search_result> 标签中获得结果。</tool_description>

请注意,此提示中有许多关于如何正确搜索维基百科的建议。我们都习惯于在谷歌上输入随机的无意义内容并获得不错的结果,因为查询解析逻辑非常好。维基百科搜索并非如此。例如:考虑查询“在阿拉伯联合酋长国购买土豆的最佳方式是什么”。在维基百科上搜索此内容的顶级命中结果 是美国奴隶制、1973 年石油危机、Wendy's 和 Tim Horton's(??)。而谷歌则正确地直接将你带到 Carrefour UAE。

另一个区别是维基百科搜索返回整个页面。使用向量搜索,你可能会获得更窄的块,因此你可能希望请求更多结果,使用更具体的查询,或两者兼而有之。大局来看,你的结果可能因你的选择而大相径庭,所以请注意!

retrieval_prompt = """在开始研究用户的问题之前,请先在 <scratchpad> 标签内思考一下回答一个信息丰富的问题需要哪些信息。如果用户的问题很复杂,你可能需要将查询分解成多个子查询并单独执行它们。有时搜索引擎会返回空的搜索结果,或者搜索结果可能不包含你需要的信息。在这种情况下,请随时尝试使用不同的查询。 

在每次调用搜索引擎工具后,请在 <search_quality></search_quality> 标签内简要反思你是否已获得足够的信息来回答,或者是否需要更多信息。如果你拥有所有相关信息,请在 <information></information> 标签中写下它,但不要实际回答问题。否则,请发出新的搜索。

这是用户的问题:<question>{query}</question> 在你的便签中提醒自己进行简短的查询,并规划你的策略。"""
print(retrieval_prompt)
在开始研究用户的问题之前,请先在 <scratchpad> 标签内思考一下回答一个信息丰富的问题需要哪些信息。如果用户的问题很复杂,你可能需要将查询分解成多个子查询并单独执行它们。有时搜索引擎会返回空的搜索结果,或者搜索结果可能不包含你需要的信息。在这种情况下,请随时尝试使用不同的查询。

在每次调用搜索引擎工具后,请在 <search_quality></search_quality> 标签内简要反思你是否已获得足够的信息来回答,或者是否需要更多信息。如果你拥有所有相关信息,请在 <information></information> 标签中写下它,但不要实际回答问题。否则,请发出新的搜索。

这是用户的问题:<question>{query}</question> 在你的便签中提醒自己进行简短的查询,并规划你的策略。

我们在这里使用便签是为了正常的思维链原因——它让 Claude 制定一个连贯的计划来回答问题。搜索质量反思用于促使 Claude 坚持不懈,并在收集所有相关信息之前不要抢先回答问题。但我们为什么要告诉 Claude 综合信息而不是立即回答呢?

answer_prompt = "这是用户查询:<query>{query}</query>。这是相关信息:<information>{information}</information>。请使用相关信息回答问题。"
print(answer_prompt)
这是用户查询:<query>{query}</query>。这是相关信息:<information>{information}</information>。请使用相关信息回答问题。

通过提取信息并将其呈现给 Claude 进行新的查询,我们允许 Claude 将所有注意力集中在将信息综合成正确的答案上。没有这一步,我们发现 Claude 有时会预先承诺一个答案,然后用搜索结果“证明”它,而不是让结果引导它。

现在是实现搜索+检索+重新提示的伪代码的代码。

搜索实现

from dataclasses import dataclass
from abc import ABC, abstractmethod
import wikipedia, re
from anthropic import Anthropic, HUMAN_PROMPT, AI_PROMPT
from typing import Tuple, Optional

@dataclass
class SearchResult:
    """
    单个搜索结果。
    """
    content: str

class SearchTool:
    """
    一个可以运行查询并返回格式化搜索结果字符串的搜索工具。
    """

    def __init__():
        pass

    @abstractmethod
    def raw_search(self, query: str, n_search_results_to_use: int) -> list[SearchResult]:
        """
        使用搜索器运行查询,然后返回未格式化的原始搜索结果。

        :param query: 要运行的查询。
        :param n_search_results_to_use: 要返回的结果数。
        """
        raise NotImplementedError()

    @abstractmethod
    def process_raw_search_results(
        self, results: list[SearchResult],
    ) -> list[str]:
        """
        从搜索结果中提取原始搜索内容,并返回可传递给 Claude 的字符串列表。

        :param results: 要提取的搜索结果。
        """
        raise NotImplementedError()

    def search_results_to_string(self, extracted: list[str]) -> str:
        """
        将提取的搜索结果连接并格式化为字符串。

        :param extracted: 要格式化的提取的搜索结果。
        """
        result = "\n".join(
            [
                f'<item index="{i+1}">\n<page_content>\n{r}\n</page_content>\n</item>'
                for i, r in enumerate(extracted)
            ]
        )
        return result

    def wrap_search_results(self, extracted: list[str]) -> str:
        """
        将提取的搜索结果格式化为字符串,包括 <search_results> 标签。

        :param extracted: 要格式化的提取的搜索结果。
        """
        return f"\n<search_results>\n{self.search_results_to_string(extracted)}\n</search_results>"

    def search(self, query: str, n_search_results_to_use: int) -> str:
        raw_search_results = self.raw_search(query, n_search_results_to_use)
        processed_search_results = self.process_raw_search_results(raw_search_results)
        displayable_search_results = self.wrap_search_results(processed_search_results)
        return displayable_search_results 
@dataclass
class WikipediaSearchResult(SearchResult):
    title: str

class WikipediaSearchTool(SearchTool):

    def __init__(self,
                 truncate_to_n_tokens: Optional[int] = 5000):
        self.truncate_to_n_tokens = truncate_to_n_tokens
        if truncate_to_n_tokens is not None:
            self.tokenizer = Anthropic().get_tokenizer()

    def raw_search(self, query: str, n_search_results_to_use: int) -> list[WikipediaSearchResult]:
        search_results = self._search(query, n_search_results_to_use)
        return search_results

    def process_raw_search_results(self, results: list[WikipediaSearchResult]) -> list[str]:
        processed_search_results = [f'Page Title: {result.title.strip()}\nPage Content:\n{self.truncate_page_content(result.content)}' for result in results]
        return processed_search_results

    def truncate_page_content(self, page_content: str) -> str:
        if self.truncate_to_n_tokens is None:
            return page_content.strip()
        else:
            return self.tokenizer.decode(self.tokenizer.encode(page_content).ids[:self.truncate_to_n_tokens]).strip()

    def _search(self, query: str, n_search_results_to_use: int) -> list[WikipediaSearchResult]:
        results: list[str] = wikipedia.search(query)
        search_results: list[WikipediaSearchResult] = []
        for result in results:
            if len(search_results) >= n_search_results_to_use:
                break
            try:
                page = wikipedia.page(result)
                print(page.url)
            except:
                # Wikipedia API有点不稳定,所以我们跳过加载失败的页面
                continue
            content = page.content
            title = page.title
            search_results.append(WikipediaSearchResult(content=content, title=title))
        return search_results
def extract_between_tags(tag: str, string: str, strip: bool = True) -> list[str]:
    ext_list = re.findall(f"<{tag}\s?>(.+?)</{tag}\s?>", string, re.DOTALL)
    if strip:
        ext_list = [e.strip() for e in ext_list]
    return ext_list

class ClientWithRetrieval(Anthropic):

    def __init__(self, search_tool: SearchTool, verbose: bool = True, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.search_tool = search_tool
        self.verbose = verbose

    # 辅助方法
    def _search_query_stop(self, partial_completion: str, n_search_results_to_use: int) -> Tuple[list[SearchResult], str]:
        search_query = extract_between_tags('search_query', partial_completion + '</search_query>') 
        if search_query is None:
            raise Exception(f'Completion with retrieval failed as partial completion returned mismatched <search_query> tags.')
        print(f'正在针对 SearchTool 运行搜索查询: {search_query}')
        search_results = self.search_tool.raw_search(search_query, n_search_results_to_use)
        extracted_search_results = self.search_tool.process_raw_search_results(search_results)
        formatted_search_results = self.search_tool.wrap_search_results(extracted_search_results)
        return search_results, formatted_search_results

    def retrieve(self,
                       query: str,
                       model: str,
                       n_search_results_to_use: int = 3,
                       stop_sequences: list[str] = [HUMAN_PROMPT],
                       max_tokens_to_sample: int = 1000,
                       max_searches_to_try: int = 5,
                       temperature: float = 1.0) -> tuple[list[SearchResult], str]:

        prompt = f"{HUMAN_PROMPT} {wikipedia_prompt} {retrieval_prompt.format(query=query)}{AI_PROMPT}"
        starting_prompt = prompt
        print("开始提示:", starting_prompt)
        token_budget = max_tokens_to_sample
        all_raw_search_results: list[SearchResult] = []
        for tries in range(max_searches_to_try):
            partial_completion = self.completions.create(prompt = prompt,
                                                     stop_sequences=stop_sequences + ['</search_query>'],
                                                     model=model,
                                                     max_tokens_to_sample = token_budget,
                                                     temperature = temperature)
            partial_completion, stop_reason, stop_seq = partial_completion.completion, partial_completion.stop_reason, partial_completion.stop
            print(partial_completion)
            token_budget -= self.count_tokens(partial_completion)
            prompt += partial_completion
            if stop_reason == 'stop_sequence' and stop_seq == '</search_query>':
                print(f'正在尝试搜索次数 {tries}。')
                raw_search_results, formatted_search_results = self._search_query_stop(partial_completion, n_search_results_to_use)
                prompt += '</search_query>' + formatted_search_results
                all_raw_search_results += raw_search_results
            else:
                break
        final_model_response = prompt[len(starting_prompt):]
        return all_raw_search_results, final_model_response

    # 主要方法
    def completion_with_retrieval(self,
                                        query: str,
                                        model: str,
                                        n_search_results_to_use: int = 3,
                                        stop_sequences: list[str] = [HUMAN_PROMPT],
                                        max_tokens_to_sample: int = 1000,
                                        max_searches_to_try: int = 5,
                                        temperature: float = 1.0) -> str:

        _, retrieval_response = self.retrieve(query, model=model,
                                                 n_search_results_to_use=n_search_results_to_use, stop_sequences=stop_sequences,
                                                 max_tokens_to_sample=max_tokens_to_sample,
                                                 max_searches_to_try=max_searches_to_try,
                                                 temperature=temperature)
        information = extract_between_tags('information', retrieval_response)[-1]
        prompt = f"{HUMAN_PROMPT} {answer_prompt.format(query=query, information=information)}{AI_PROMPT}"
        print("正在总结:\n", prompt)
        answer = self.completions.create(
            prompt = prompt, model=model, temperature=temperature, max_tokens_to_sample=1000
        ).completion
        return answer

运行查询

我们准备执行一个查询!让我们选择一些内容:

  • 最近的,所以它不太可能在 Claude 的训练数据中,并且
  • 复合/复杂的,所以它需要多次搜索。
import os
# 创建一个搜索器
wikipedia_search_tool = WikipediaSearchTool()
ANTHROPIC_SEARCH_MODEL = "claude-2"

client = ClientWithRetrieval(api_key=os.environ['ANTHROPIC_API_KEY'], verbose=True, search_tool = wikipedia_search_tool)

query = "Which movie came out first: Oppenheimer, or Are You There God It's Me Margaret?"

augmented_response = client.completion_with_retrieval(
    query=query,
    model=ANTHROPIC_SEARCH_MODEL,
    n_search_results_to_use=1,
    max_searches_to_try=5,
    max_tokens_to_sample=1000,
    temperature=0)
print(augmented_response)
开始提示:

Human: 你将由一位人类用户提出问题。你可以使用以下工具来帮助回答问题。 <tool_description> 搜索引擎工具 * 搜索引擎将专门搜索与你的查询相似的维基百科页面。它为每个页面返回其标题和完整的页面内容。如果你想获取关于某个主题的最新和全面的信息来帮助回答查询,请使用此工具。查询应尽可能原子化——它们只需要解决用户问题的一部分。例如,如果用户的问题是“篮球的颜色是什么?”,你的搜索查询应该是“篮球”。再举一个例子:如果用户的问题是“谁创建了第一个神经网络?”,你的第一个查询应该是“神经网络”。正如你所见,这些查询非常简短。关键词优先,而非短语。 * 你可以随时使用以下语法调用搜索引擎:<search_query>查询词</search_query>。 * 然后你将在 <search_result> 标签中获得结果。</tool_description> 在开始研究用户的问题之前,请先在 <scratchpad> 标签内思考一下回答一个信息丰富的问题需要哪些信息。如果用户的问题很复杂,你可能需要将查询分解成多个子查询并单独执行它们。有时搜索引擎会返回空的搜索结果,或者搜索结果可能不包含你需要的信息。在这种情况下,请随时尝试使用不同的查询。

在每次调用搜索引擎工具后,请在 <search_quality></search_quality> 标签内简要反思你是否已获得足够的信息来回答,或者是否需要更多信息。如果你拥有所有相关信息,请在 <information></information> 标签中写下它,但不要实际回答问题。否则,请发出新的搜索。

这是用户的问题:<question>Which movie came out first: Oppenheimer, or Are You There God It's Me Margaret?</question> 在你的便签中提醒自己进行简短的查询,并规划你的策略。

Assistant:
 <scratchpad>
为了回答这个问题,我需要找到:

- 《奥本海默》的上映日期
- 《你是上帝吗?是我,玛格丽特》的上映日期
我可以分别搜索每个电影标题来获取上映日期。
</scratchpad>

<search_query>Oppenheimer movie
正在尝试搜索次数 0。
正在针对 SearchTool 运行搜索查询: ['Oppenheimer movie']
https://en.wikipedia.org/wiki/Oppenheimer_(film)


搜索结果显示《奥本海默》定于 2023 年 7 月 21 日上映。这提供了《奥本海默》的上映日期。

<search_quality>搜索结果直接提供了《奥本海默》的上映日期,因此我现在拥有回答问题第一部分所需的信息。</search_quality>

<search_query>Are You There God It's Me Margaret movie
正在尝试搜索次数 1。
正在针对 SearchTool 运行搜索查询: ["Are You There God It's Me Margaret movie"]
https://en.wikipedia.org/wiki/Are_You_There_God%3F_It%27s_Me,_Margaret.


搜索结果显示,《你是上帝吗?是我,玛格丽特》的电影改编版于 2023 年 4 月 28 日上映。这提供了《你是否是上帝?是我,玛格丽特》的上映日期。

<search_quality>搜索结果直接说明了《你是否是上帝?是我,玛格丽特》电影改编版的上映日期,因此我现在拥有完全回答问题所需的所有信息。</search_quality>

<information>

- 《奥本海默》于 2023 年 7 月 21 日上映
- 《你是否是上帝?是我,玛格丽特》于 2023 年 4 月 28 日上映
</information>

根据我搜索到的上映日期,《奥本海默》先上映,于 2023 年 7 月 21 日上映,而《你是否是上帝?是我,玛格丽特》于 2023 年 4 月 28 日上映。
正在总结:


Human:这是用户查询:<query>Which movie came out first: Oppenheimer, or Are You There God It's Me Margaret?</query>。这是相关信息:<information>- 《奥本海默》于 2023 年 7 月 21 日上映

- 《你是否是上帝?是我,玛格丽特》于 2023 年 4 月 28 日上映</information>。请使用相关信息回答问题。

Assistant:
 根据提供的信息,《你是否是上帝?是我,玛格丽特》先于 2023 年 4 月 28 日上映。而《奥本海默》于 2023 年 7 月 21 日上映。

太棒了,Claude 能够制定计划、执行查询并将信息综合成准确的答案。注意:如果没有额外的信息提取步骤,Claude 有时会正确确定电影的上映日期,但在最终答案中会弄错顺序。我们再来一个。

augmented_response = client.completion_with_retrieval(
    query="Who won the 2023 NBA championship? Who was that team's best player in the year 2009?",
    model=ANTHROPIC_SEARCH_MODEL,
    n_search_results_to_use=1,
    max_searches_to_try=5,
    max_tokens_to_sample=1000,
    temperature=0)
print(augmented_response)
开始提示:

Human: 你将由一位人类用户提出问题。你可以使用以下工具来帮助回答问题。 <tool_description> 搜索引擎工具 * 搜索引擎将专门搜索与你的查询相似的维基百科页面。它为每个页面返回其标题和完整的页面内容。如果你想获取关于某个主题的最新和全面的信息来帮助回答查询,请使用此工具。查询应尽可能原子化——它们只需要解决用户问题的一部分。例如,如果用户的问题是“篮球的颜色是什么?”,你的搜索查询应该是“篮球”。再举一个例子:如果用户的问题是“谁创建了第一个神经网络?”,你的第一个查询应该是“神经网络”。正如你所见,这些查询非常简短。关键词优先,而非短语。 * 你可以随时使用以下语法调用搜索引擎:<search_query>查询词</search_query>。 * 然后你将在 <search_result> 标签中获得结果。</tool_description> 在开始研究用户的问题之前,请先在 <scratchpad> 标签内思考一下回答一个信息丰富的问题需要哪些信息。如果用户的问题很复杂,你可能需要将查询分解成多个子查询并单独执行它们。有时搜索引擎会返回空的搜索结果,或者搜索结果可能不包含你需要的信息。在这种情况下,请随时尝试使用不同的查询。

在每次调用搜索引擎工具后,请在 <search_quality></search_quality> 标签内简要反思你是否已获得足够的信息来回答,或者是否需要更多信息。如果你拥有所有相关信息,请在 <information></information> 标签中写下它,但不要实际回答问题。否则,请发出新的搜索。

这是用户的问题:<question>Who won the 2023 NBA championship? Who was that team's best player in the year 2009?</question> 在你的便签中提醒自己进行简短的查询,并规划你的策略。

Assistant:
 <scratchpad>
为了回答这个问题,我需要找到:

1. 赢得 2023 年 NBA 总冠军的球队
2. 该球队在 2009 年的最佳球员
我可以分别搜索这些内容。
</scratchpad>

<search_query>2023 nba championship winner
正在尝试搜索次数 0。
正在针对 SearchTool 运行搜索查询: ['2023 nba championship winner']
https://en.wikipedia.org/wiki/List_of_NBA_champions


<search_quality>搜索结果包含了赢得 2023 年 NBA 总冠军的球队,因此我已获得回答问题第一部分所需的信息。</search_quality>

<information>
丹佛掘金队赢得了 2023 年 NBA 总冠军。
</information>

<search_query>denver nuggets best player 2009
正在尝试搜索次数 1。
正在针对 SearchTool 运行搜索查询: ['denver nuggets best player 2009']
https://en.wikipedia.org/wiki/2009%E2%80%9310_Denver_Nuggets_season


<search_quality>搜索结果表明卡梅洛·安东尼是掘金队在 2009 年的最佳球员,因此我现在拥有完全回答问题所需的所有信息。</search_quality>

<information>

- 丹佛掘金队赢得了 2023 年 NBA 总冠军。
- 卡梅洛·安东尼是掘金队在 2009 年的最佳球员。
</information>
Summarizing:


Human:这是用户查询:<query>Who won the 2023 NBA championship? Who was that team's best player in the year 2009?</query>。这是相关信息:<information>- 丹佛掘金队赢得了 2023 年 NBA 总冠军。

- 卡梅洛·安东尼是掘金队在 2009 年的最佳球员。</information>。请使用相关信息回答问题。

Assistant:
 <response>
根据提供的信息:

- 丹佛掘金队赢得了 2023 年 NBA 总冠军。 
- 卡梅洛·安东尼是掘金队在 2009 年的最佳球员。
</response>

看吧!你可能会注意到搜索工具代码很简洁且抽象,可以通过少量修改来适应使用你选择的搜索 API。只需记住向 Claude 解释它需要了解的有关有效使用该工具的技巧。你甚至可以为 Claude 提供一些理想的查询计划和查询结构的示例,以进一步提高性能。