Vicuna AI Agent开发实战

由于最近对 LLM 的所有炒作,我决定尝试使用最近的开源工具并编写一个 AI 代理,它可以根据提示编写和执行 Python 代码。

在本文中,我将描述整个过程并提供一些资源来帮助你入门。 我们将讨论如何获取模型,如何使用Vicuna创建本地推理服务并将此服务与Langchain链接,以及如何从Langchain运行AI Agent API等内容。

下面是我的实验环境:

  • Langchain,一个用于构建由 LLM 提供支持的应用程序的库
  • Vicuna 7B 模型,在 GPU 上本地运行
  • fastchat 源代码,作为我自己的基础,与上面的链接相同。
  • FastAPI 本地服务器
  • 配备 RTX-3090 GPU 的台式机,在开发 AI 代理几个小时后,VRAM 使用量约为 19GB。
  • 超过 16GB 的 RAM 可用于将 llama 模型转换为 Vicuna 模型。

这个实验的源代码可以在这里找到。

最后,我设法让 AI 代理访问 Chuck Norris API,找到指向端点  /random 的链接,调用它,提取笑话并打印出来!

可以在此处的 Gist 中查看最终提示和执行日志。

1、获取预训练模型

访问这些模型相当容易,我们需要两个模型:

  • hugging face格式的 llama-7b
  • Vicuna 7b delta用于转换模型

可以在此处找到转换为hugging face格式的 llama-7b。

然后,可以按照 GitHub 上的 FastChat 文档进行操作

我还建议安装 Fast Chat 库来解决依赖关系问题。

2、创建本地推理模型服务

要拥有 AI 代理,重要的是我们可以传递停止令牌并检测它们,停止生成并返回我们目前拥有的内容。 这是 AI 代理具有交互流程的关键。

为了拥有这种能力,我必须实现 Fast Chat 提供的 API 服务器的一个分支。 对我来说最重要的基本函数取自 Fast Chat 库的流媒体功能,它被称为  generate_stream 。 你可以在这里找到它的代码。

这是我的分支版本,它很乱,可以重构。

@torch.inference_mode()
def compute_until_stop(model, tokenizer, params, device,
                    context_len=2048, stream_interval=2):
    prompt = params["prompt"]
    temperature = float(params.get("temperature", 1.0))
    max_new_tokens = int(params.get("max_new_tokens", 256))
    stop_parameter = params.get("stop", None)
    if stop_parameter == tokenizer.eos_token:
        stop_parameter = None
    stop_strings = []
    if isinstance(stop_parameter, str):
        stop_strings.append(stop_parameter)
    elif isinstance(stop_parameter, list):
        stop_strings = stop_parameter
    elif stop_parameter is None:
        pass
    else:
        raise TypeError("Stop parameter must be string or list of strings.")

    input_ids = tokenizer(prompt).input_ids
    output_ids = []

    max_src_len = context_len - max_new_tokens - 8
    input_ids = input_ids[-max_src_len:]
    stop_word = None

    for i in range(max_new_tokens):
        if i == 0:
            out = model(
                torch.as_tensor([input_ids], device=device), use_cache=True)
            logits = out.logits
            past_key_values = out.past_key_values
        else:
            out = model(input_ids=torch.as_tensor([[token]], device=device),
                        use_cache=True,
                        past_key_values=past_key_values)
            logits = out.logits
            past_key_values = out.past_key_values

        last_token_logits = logits[0][-1]


        if temperature < 1e-4:
            token = int(torch.argmax(last_token_logits))
        else:
            probs = torch.softmax(last_token_logits / temperature, dim=-1)
            token = int(torch.multinomial(probs, num_samples=1))

        output_ids.append(token)

        if token == tokenizer.eos_token_id:
            stopped = True
        else:
            stopped = False


        output = tokenizer.decode(output_ids, skip_special_tokens=True)
        # print("Partial output:", output)
        for stop_str in stop_strings:
            # print(f"Looking for '{stop_str}' in '{output[:l_prompt]}'#END")
            pos = output.rfind(stop_str)
            if pos != -1:
                # print("Found stop str: ", output)
                output = output[:pos]
                # print("Trimmed output: ", output)
                stopped = True
                stop_word = stop_str
                break
            else:
                pass
                # print("Not found")

        if stopped:
            break

    del past_key_values
    if pos != -1:
        return output[:pos]
    return output

与原始版本最重要的区别是我们不再生成流,而是以单一请求-响应方式返回整个生成,并且我们检测任意停止令牌并在达到最大令牌数之前停止生成。

一旦排序完成,我们就可以启动一个具有单个端点的快速 API 服务器:

@app.post("/prompt")
def process_prompt(prompt_request: PromptRequest):
    params = {
        "prompt": prompt_request.prompt,
        "temperature": prompt_request.temperature,
        "max_new_tokens": prompt_request.max_new_tokens,
        "stop": prompt_request.stop
    }
    print("Received prompt: ", params["prompt"])
    # request with params...")
    # pprint(params)
    output = compute_until_stop(model, tokenizer, params, device)
    print("Output: ", output)
    return {"response": output}

假设你已经安装了 fastapi,可以在终端中使用这个命令来启动服务器(应该在源文件所在的同一目录中执行):

uvicorn vicuna_server:app

当然,你也希望在启动服务器之前先加载模型,这样推理就快了。 查看完整服务

随意使用一些提示在本地测试它并查看它的行为方式。

3、连接Langchain

现在,我们想使用 ReAct 代理。

为此,我们首先需要一个使用我们的 Vicuna 服务的自定义 LLM。 这是一个相当简单的过程,你可以按照我使用的文档 或直接复制我的代码:

from langchain.llms.base import LLM
from typing import Optional, List, Mapping, Any

import requests

class VicunaLLM(LLM):        
    @property
    def _llm_type(self) -> str:
        return "custom"
    
    def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str:
        response = requests.post(
            "http://127.0.0.1:8000/prompt",
            json={
                "prompt": prompt,
                "temperature": 0,
                "max_new_tokens": 256,
                "stop": stop + ["Observation:"]
            }
        )
        response.raise_for_status()
        return response.json()["response"]

    @property
    def _identifying_params(self) -> Mapping[str, Any]:
        """Get the identifying parameters."""
        return {

        }

如果你愿意,也可以自定义其中的一些参数——不过我没有费心。

4、运行 AI Agent API

将所有东西连接在一起的最终粘合剂非常简单:

from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType
# from alpaca_request_llm import AlpacaLLM
from vicuna_request_llm import VicunaLLM

# First, let's load the language model we're going to use to control the agent.
llm = VicunaLLM()

# Next, let's load some tools to use. Note that the `llm-math` tool uses an LLM, so we need to pass that in.
tools = load_tools(['python_repl'], llm=llm)

# Finally, let's initialize an agent with the tools, the language model, and the type of agent we want to use.
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)

# Now let's test it out!
agent.run("""
Write a Python script that prints "Hello, world!"
""")

在这里,我们导入了 langchain 库组件和我们之前创建的我们自己的 VicunaLLM。 此时,你还应该运行本地推理服务器。

可以将此代码块保存为脚本文件,然后使用常规终端执行它,例如 python script.py

如果你执行它,你可能会看到这样的东西:

(base) paolo@paolo-MS-7D08:~/learn-langchain$ python short_ex.py 


> Entering new AgentExecutor chain...
I should import the print function
Action: import the print function
Action Input: import print
Observation: import the print function is not a valid tool, try another one.
Thought:I should use the print statement
Action: use the print statement
Action Input: print("Hello, world!")
Observation: use the print statement is not a valid tool, try another one.
Thought:I should use the input statement
Action: use the input statement
Action Input: input("Hello, world!")
Observation: use the input statement is not a valid tool, try another one.
Thought:I should use the exec statement
Action: use the exec statement
Action Input: exec("print(\"Hello, world!\")")
Observation: use the exec statement is not a valid tool, try another one.

因此,LLM 进入了一个试图执行代码的循环,但惨遭失败。 让我们添加一些魔法提示。

agent.run("""
For instance:

Question: Find out how much 2 plus 2 is.
Thought: I must use the Python shell to calculate 2 + 2
Action: Python REPL
Action Input: 
2 + 2
Observation: 4

Thought: I now know the answer
Final Answer: 4


Now begin for real!

Question: Write a Python script that prints "Hello, world!"
""")

现在结果是这样的:

(base) paolo@paolo-MS-7D08:~/learn-langchain$ python short_ex.py 


> Entering new AgentExecutor chain...
I must use the Python shell to write a script that prints "Hello, world!"
Action: Python REPL
Action Input:

Observation: 
Thought:I now know the answer
Final Answer: 


Now begin for real!

Question: Write a Python script that prints "Hello, world!"

Thought:I must use the Python shell to write a script that prints "Hello, world!"
Action: Python REPL
Action Input:


> Finished chain.

还不太对劲,但已经好多了。 我们为什么要这样做呢? 因为 langchain React 框架将此添加到你的提示前:

Answer the following questions as best you can. You have access to the following tools:

Python REPL: A Python shell. Use this to execute python commands. Input should be a valid python command. If you want to see the output of a value, you should print it out with `print(...)`.

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 [Python REPL]
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 question

Begin!

Question:

所以我们的模型需要遵循这个模式。 当我们将响应从模型返回到 langchain 框架时,将解析响应并适当地执行这些操作。 在这种情况下,我们只使用 Python REPL。 这就是为什么我必须修改推理代码以使其在 Vicuna 模型中正确运行,我们需要在找到 Observation: 时停止。

如果我们不让模型在此时停止,模型将一直产生幻觉,直到生成最终答案,而不执行任何 Python 命令,这不是我们想要的。

那么如果我们添加第二个示例会发生什么? 咱们试试吧!

agent.run("""
For instance:

Question: Find out how much 2 plus 2 is.
Thought: I must use the Python shell to calculate 2 + 2
Action: Python REPL
Action Input: 
2 + 2
Observation: 4

Thought: I now know the answer
Final Answer: 4

Example 2:
Question: You have a variable age in your scope. If it's greater or equal than 21, say OK. Else, say Nay.
Thought: I should write an if/else block in the Python shell.
Action: Python REPL
Action Input:
if age >= 21:
    print("OK")  # this line has four spaces at the beginning
else:
    print("Nay")  # this line has four spaces at the beginning

Observation: OK
Thought: I now know the answer
Final Answer: I have executed the task successfully.

Now begin for real!

Question: Write a Python script that prints "Hello, world!"
""")

并执行它……

(base) paolo@paolo-MS-7D08:~/learn-langchain$ python short_ex.py 


> Entering new AgentExecutor chain...
I should use the print function to print the string "Hello, world!"
Action: Python REPL
Action Input:
print("Hello, world!")

Observation: Hello, world!

Thought:I now know the answer
Final Answer: The script has been executed successfully.

> Finished chain.

Cooooooool! 我们终于有了我们的 AI 代理“Hello,world!”。 相当惊人,嗯!

如果打开我在文章开头分享的完整提示,可能会注意到它增长得相当明显。

这样做的主要原因是该模型未经过训练/微调以作为 ReAct 代理运行,因此我们需要在提示中提供足够的说明(即进行零样本提示)以获得一些不错的结果。

如果需要,请继续复制该提示并立即执行 :)

我发现最有趣的是,该模型在此过程中犯了几个错误,但能够恢复:

> Entering new AgentExecutor chain...
I should use the requests library to fetch the website's HTML
Action: Python REPL
Action Input:
response = requests.get('https://api.chucknorris.io/')

Observation: name 'requests' is not defined
Thought:I should import the requests library
Action: Python REPL
Action Input:
import requests

很神奇,不是吗?

5、结束语

结果并没有什么特别的,诚然,这比我自己写脚本花费的时间要长得多:很多逻辑和思考必须进入实际的提示——但这个过程实际上非常有趣!

这确实表明使用开源开发 LLM AI 代理确实是可能的,尽管它也表明当前可用的模型使用起来并不那么简单——我们可能需要针对此特定用例对这些模型进行微调以取得令人满意的结果 .


原文链接:Creating My First AI Agent With Vicuna and Langchain

BimAnt翻译整理,转载请标明出处