迭代搜索维基百科与 Claude
[免责声明:本笔记本使用 Claude 2 模型创建,现已视为旧版。]
有些问题 Claude 无法凭空回答。也许它们是关于时事的。也许你有一个 Claude 没有记住答案的极其详细的问题。没关系!通过一些提示和脚手架,Claude 可以搜索网络来找到答案。在本笔记本中,我们将创建一个虚拟研究助手,它能够搜索维基百科来找到你问题的答案。相同的方法也可用于允许 Claude 搜索更广泛的网络,或你提供的一组文档。
方法是什么?大体上它属于“工具使用”类别。我们创建一个搜索工具,告诉 Claude 关于它的信息,然后让它开始工作。用伪代码表示:
- 用搜索工具的描述、如何最好地使用它以及如何“调用”它(通过发出特殊字符串)来提示 Claude。
- 告诉 Claude 你要问的问题。
- Claude 像往常一样生成 token。如果它生成了特殊字符串,则终止 token 生成流,并向搜索 API 发出查询。
- 构建一个新提示,该提示由步骤 1 中的提示、Claude 生成的所有内容直到搜索调用字符串以及 API 调用结果组成。
- 重复此过程,直到 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 提供一些理想的查询计划和查询结构的示例,以进一步提高性能。