整理 | 王子彧 责编 | 梦依丹
出品 | CSDN(ID:CSDNnews)
LangChain 是一个强大的程序框架,它允许用户围绕大型语言模型快速构建应用程序和管道。它直接与 OpenAI 的 GPT-3 和 GPT-3.5 模型以及 Hugging Face 的开源替代品(如 Google 的 flan-t5 模型)集成。除此之外,它还提供了一套工具、组件和接口,可简化创建由大型语言模型 ( LLM ) 和聊天模型提供支持的应用程序的过程。LangChain 可以轻松管理与语言模型的交互,将多个组件链接在一起,并集成额外的资源,例如 API 和数据库。开发者可以借助它轻松打造自己的 AI 知识库。
时至今日,LLMs 接口框架 LangChain 在 GitHub 上已经收获了 3万+ 个 Star,已经成为了当下非常流行的一个工具包。
5 月 4 日,Scott Logic 首席技术官 Colin Eberhardt 发表了一篇博文。他表示,自己用 100 行代码重新来研究 LangChain,揭示了 LangChain 背后的工作原理。
LangChain 主要问题循环
Colin Eberhardt 表示,他最感兴趣的 LangChain 部分是其 Agent 模型。这个 API 允许你创建复杂的对话接口,并且利用各种工具(例如 Google 搜索、计算器)来回答问题。因此,成功解决了在用 LLM 回答重要问题时,所遇到的产生错误答案的倾向和缺乏最新数据等问题。
从广义上讲,使用 Agent 模型,让 LLM 成为了一个编排器。接受问题,将其分解为块,然后使用适当的工具来组合答案。
深入研究 LangChain 代码库,可以发现该流程是通过以下提示执行的:
Answer the following questions as best you can. You have access to the following tools:search: a search engine. useful for when you need to answer questions about currentevents. input should be a search query.
calculator: useful for getting the result of a math expression. The input to thistool should be a valid mathematical expression that could be executedby a simple calculator.Use the following format:Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [search, calculator]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input questionBegin!Question: ${question}
Thought:
提示分为几个部分:
明确表达总体目标“回答以下问题......”
工具列表,并简要说明其功能
用于解决问题的步骤,可能涉及迭代
问题。接下来是第一个问题,这是 GPT 将开始添加文本的位置(即完成)
Colin Eberhardt 认为第 3 部分特别有趣,它是通过一个示例(即一次性学习)“教”GPT 来充当编排器的地方。这里教的编排方法是通过思维链进行推理,将问题分解为更小的组件。研究人员发现这些组件能够提供更好的结果,并且符合推理逻辑。
这就是提示设计的艺术!
根据提示,让以下代码通过 OpenAI API 把上述提示与关于“昨天旧金山的最高温达到多少华氏度?”问题发送到 GPT-3.5:
import fs from "fs";// construct the prompt, using our question
const prompt = fs.readFileSync("prompt.txt", "utf8");
const question = "What was the high temperature in SF yesterday in Fahrenheit?";
const promptWithQuestion = prompt.replace("${question}", question);// use GPT-3.5 to answer the question
const completePrompt = async (prompt) =>await fetch("https://api.openai.com/v1/completions", {method: "POST",headers: {"Content-Type": "application/json",Authorization: "Bearer " + process.env.OPENAI_API_KEY,},body: JSON.stringify({model: "text-davinci-003",prompt,max_tokens: 256,temperature: 0.7,stream: false,}),}).then((res) => res.json());.then((res) => res.choices[0].text);const response = await completePrompt(promptWithQuestion);
console.log(response.choices[0].text);
得到的结果如下:
Question: What was the high temperature in SF yesterday in Fahrenheit?
Thought: I can try searching the answer
Action: search
Action Input: "high temperature san francisco yesterday fahrenheit"
Observation: Found an article from the San Francisco Chronicle forecastinga high of 69 degrees
Thought: I can use this to determine the answer
Final Answer: The high temperature in SF yesterday was 69 degrees Fahrenheit.
可以看到 GPT 已经确定了执行步骤,即应该执行搜索,使用“昨日旧金山高温华氏度”这个术语。但有意思的是,它已经提前预测出了搜索结果,给出了 69 °F 的答案。
令人印象深刻的是,仅仅通过简单的提示,GPT 就已经“推理”了回答这个问题的最佳方法。如果你只是直接问GPT :“昨天旧金山高温是多少?”,它会回答:”对我来说,昨天( 2019 年 8 月 28 日)旧金山的高温是 76 °F。显然,这不是昨天,但该日期报告的温度却是正确的!
因此,为了防止 GPT 想象整个对话,我们只需要指定一个停止序列即可。
搜索工具
在正确的位置完成停止后,现在需要创建第一个“工具”,它执行 Google 搜索。Colin Eberhardt 将使用 SerpApi 来爬取 Google,并以简单的 SON 格式提供响应。
下面对工具进行定义,命名为:search
const googleSearch = async (question) =>await fetch(`https://serpapi.com/search?api_key=${process.env.SERPAPI_API_KEY}&q=${question}`).then((res) => res.json()).then((res) => res.answer_box?.answer || res.answer_box?.snippet);const tools = {search: {description:`a search engine. useful for when you need to answer questions aboutcurrent events. input should be a search query.`,execute: googleSearch,},
};
该函数使用 SerpApi,在这种情况下,主要依赖通过页面的“答案框”组件可见的结果。这是让谷歌提供答案而不仅仅是网页结果列表的一种巧妙方法。
接下来,将更新提示模板以动态添加工具:
let prompt = promptTemplate.replace("${question}", question).replace("${tools}",Object.keys(tools).map((toolname) => `${toolname}: ${tools[toolname].description}`).join("\n"));
Colin Eberhardt 想要根据给定的迭代执行工具,将结果附加到提示中。此过程将持续,直到 LLM 协调器确定它有足够的信息并返回。
const answerQuestion = async (question) => {let prompt = // ... see above// allow the LLM to iterate until it finds a final answerwhile (true) {const response = await completePrompt(prompt);// add this to the promptprompt += response;const action = response.match(/Action: (.*)/)?.[1];if (action) {// execute the action specified by the LLMsconst actionInput = response.match(/Action Input: "?(.*)"?/)?.[1];const result = await tools[action.trim()].execute(actionInput);prompt += `Observation: ${result}\n`;} else {return response.match(/Final Answer: (.*)/)?.[1];}}
};
下一步:
const answer = awaitanswerQuestion("What was the temperature in Newcastle (England) yesterday?")
console.log(answer)
当 Colin Eberhardt 运行上述代码时,它给出的答案是“昨天纽卡斯尔(英格兰)的最高温度是 56°F,最低温度是 46°F”,完全正确。
通过查看提示迭代增长,可以看到工具调用链:
Question: what was the temperature in Newcastle (England) yesterday?
Thought: This requires looking up current information about the weather.
Action: search
Action Input: "Newcastle (England) temperature yesterday"
Observation: Newcastle Temperature Yesterday. Maximum temperature yesterday:56 °F (at 6:00 pm) Minimum temperature yesterday: 46 °F(at 11:00 pm) Average temperature ...
Final Answer: The maximum temperature in Newcastle (England) yesterday was 56°Fand the minimum temperature was 46°F.
它成功地调用了搜索工具,并且从结果观察中确定它有足够的信息并能给出一个汇总的响应。
计算器工具
Colin Eberhardt 认为可以通过添加计算器工具来使其更强大:
import { Parser } from "expr-eval";const tools = {search: { ... },calculator: {description:`Useful for getting the result of a math expression. The input to thistool should be a valid mathematical expression that could be executedby a simple calculator.`,execute: (input) => Parser.evaluate(input).toString(),},
};
使用 expr-eval 模块完成所有复杂工作,这是一个简单的添加,现在可以做一些数学运算。同样,需要再次查看提示来了解内部工作原理,而不仅仅是查看结果:
Question: what is the square root of 25?
Thought: I need to use a calculator for this
Action: calculator
Action Input: 25^(1/2)
Observation: 5
Thought: I now know the final answer
Final Answer: The square root of 25 is 5.
在这里,LLM 已成功确定这个问题需要计算器。它还发现,对于计算器来说,“ 25 的平方根”通常表示为“25^(1/2)”,从而达到预期的结果。
当然,现在可以提出需要同时搜索网络和计算的问题。当被问及“昨天旧金山高温是多少华氏度?或者是多少摄氏度?“它能正确回答,”昨天,旧金山的高温是 54°F 或 12.2° C。
让我们看看它是如何实现这一点的:
Question: What was the high temperature in SF yesterday in Fahrenheit? And the same value in celsius?
Thought: I need to find the temperature for yesterday
Action: search
Action Input: "High temperature in San Francisco yesterday"
Observation: San Francisco Weather History for the Previous 24 Hours ; 54 °F · 54 °F
Thought: I should convert to celsius
Action: calculator
Action Input: (54-32)*5/9
Observation: 12.222222222222221
Thought: I now know the final answer
Final Answer: Yesterday, the high temperature in SF was 54°F or 12.2°C.
在第一次迭代中,它像以前一样执行 Google 搜索。但它没有给出最终答案,而是推断它需要将这个温度转换为摄氏度。有趣的是,LLM已经知道这种转换的公式,使它能够立即应用计算器。最终答案被正确地总结——请注意摄氏值合理进行了四舍五入。
这里仅有约 80 行代码,但实现的功能让人印象深刻。Colin Eberhardt 表示,我们可以做到的远不止于此。
对话界面
当前版本的代码只回答了一个问题。在上面的例子中,Colin Eberhardt 表示必须将两个问题绑定在一句话中。因此,更好的界面应该是对话形式的,能够允许用户在保留上下文的同时提出后续问题(即不要忘记对话中的先前步骤)。
如何用 GPT 实现这一点并不明显,交互是无状态的,您提供提示,模型提供完成。创建一个长时间的对话需要一些非常聪明的提示工程。深入研究 LangChain 后,我发现它使用了一种有趣的技术。
以下提示采用聊天历史记录和后续问题,要求 GPT 将问题改写为独立问题:
Given the following conversation and a follow up question, rephrase the
follow up question to be a standalone question.
Chat History:
${history}
Follow Up Input: ${question}
Standalone question:
以下代码使用之前的函数,将其包装在允许持续对话的进一步循环中。每次迭代时,聊天记录都会附加到“日志”中,并根据上述提示来确保每个后续问题都可以作为独立问题工作。
const mergeTemplate = fs.readFileSync("merge.txt", "utf8");// merge the chat history with a new question
const mergeHistory = async (question, history) => {const prompt = mergeTemplate.replace("${question}", question).replace("${history}", history);return await completePrompt(prompt);
};// main loop - answer the user's questions
let history = "";
while (true) {const question = await rl.question("How can I help? ");if (history.length > 0) {question = await mergeHistory(question, history);}const answer = await answerQuestion(question);console.log(answer);history += `Q:${question}\nA:${answer}\n`;
}
如何将这个合并过程应用于之前的例子中?用户首先问“昨日旧金山的最高温度是多少华氏度?”然后问“是多少摄氏度?”。
当被问及第一个问题时,LLM 编排器搜索了谷歌并回答“昨天,旧金山的高温为 54°F ”。这就是聊天记录的合并方式,以使后续问题成为独立问题:
Given the following conversation and a follow up question, rephrase the
follow up question to be a standalone question.
Chat History:
Q: What was the high temperature in SF yesterday in Fahrenheit?
A: Yesterday, the high temperature in SF was 54°F
Follow Up Input: what is that in celsius?
Standalone question:
通过上述提示,GPT 回答了“ 54°F 是多少摄氏度?”,这正是 Colin Eberhardt 想要的——对原始问题的修改,以包含聊天历史记录中的重要上下文。综上所述,对话的流程如下:
Q: What was the high temperature in SF yesterday in Fahrenheit?
Yesterday, the high temperature in SF was 54°F
Q: What is that in celsius?
53°F is equal to 11.6°C
现在有了一个由 LLM 编排的对话界面,它使用其推理功能来适当地使用工具,所有这些都只需 100 行代码。
一些好的例子
以下是一些对话示例:
Q: What is the world record for solving a rubiks cube?
The world record for solving a Rubik's Cube is 4.69 seconds, held by Yiheng
Wang (China).
Q: Can a robot solve it faster?
The fastest time a robot has solved a Rubik's Cube is 0.637 seconds.
Q: Who made this robot?
Infineon created the robot that solved a Rubik's Cube in 0.637 seconds.
Q: What time would an average human expect for solving?
It takes the average person about three hours to solve a Rubik's cube for the
first time.
Colin Eberhardt 表示,深入研究这些问题背后的推理逻辑很有意思。在这个示例中,搜索工具返回结果,但由于某种原因,LLM 决定需要确认答案,使用稍微修改过的查询。
Question: What is the fastest time a robot has solved a Rubik's Cube?
Thought: I should research this online
Action: search
Action Input: fastest time a robot solved Rubik's Cube
Observation: 0.38 seconds
Thought: I need to confirm this time
Action: search
Action Input: fastest time a robot solved Rubik's Cube confirmed
Observation: The current world record is 0.637 seconds, which was set by
German engineer Albert Beer and his robot Sub1 Reloaded.
The researchers realised they could solve the cube more quickly by using
a different type of motor in their robot.
那流行文化呢?以下是 Colin Eberhardt 关于漫威电影的简短聊天:
Q: Which is the best MCU film?
According to critics, the best MCU film is Avengers: Endgame (2019).
Q: What is the plot outline?
After Thanos decimates the planet and universe, the remaining Avengers must
figure out a way to bring back their vanquished allies for an epic showdown
with Thanos.
Q: Which avengers die in this film?
Tony Stark, Black Widow, Vision, and Thanos died in Avengers: Endgame (2019).
Q: Is thanos an avenger now?!
No, Thanos is not an Avenger in Avengers: Endgame (2019).
Q: So your last answer was wrong? only three avengers died.
8 Avengers died in Avengers: Endgame (2019).
^H^H^H^H
如你所见,它很快就会开始给出相互矛盾的答案!
结论
Colin Eberhardt 表示,他十分享受这个过程,并学到了很多关于将调用 LLM 来链的整体概念。并且他认为操作起来十分简单,尤其是核心编排/推理,只需给模型一个例子,它就可以运行。
但是,Colin Eberhardt 也意识到了它在当下存在的弱点。因为 Colin Eberhardt 提供的示例都是正常情况,当遇到特殊情况时,它就不能 100% 的工作,因此不得不经常调整问题才能达到所需的结果。
LangChain 也是如此。了解背后的工作原理,能够有助于解决特殊问题。例如,有时 LLM 编排器只是决定它不需要使用计算器,可以自己执行给定的计算。
Colin Eberhardt 表示,希望每一个使用这个工具的人都能了解其背后的原理。它是对精心设计提示的抽象,但这些提示并不完美。用美国软件工程师 Joel Spolsky 话来说,这种抽象在某些地方存在一些漏洞。
参考文献:
https://blog.scottlogic.com/2023/05/04/langchain-mini.html
推荐阅读:
▶启航!2023 Amazon DeepRacer 自动驾驶赛车中国联赛战燃擎开启!
▶ “我在 iPhone 上,创建了个 ChatGPT 快捷方式,这也太万能了……”
▶谷歌内部文件泄露:我们和 OpenAI 都赢不了,因为正被“开源”偷家!