当前位置:   article > 正文

AI大模型时代下运维开发探索第一篇:ReAct工程初探_react 大模型

react 大模型

引子

人工智能大模型的出现,已渐渐地影响了我们的日常生活和工作方式。生活中无处不在的AI,使我们的生活变得更加智能和便捷。工作中,AI大模型的高效和精准,极大地提升了我们解决问题的效率。

是的,我们不能忽视AI大模型对运维开发的巨大影响和潜力。本系列文章旨在探索这一可能性,试图解答一个问题——AI大模型是否能够融入我们的运维开发中,为我们带来更大的便利和价值。我们期待通过这个探索,找到一个AI大模型与运维开发相融合的新方向,开启一种崭新的、更高效的运维开发方式。

真正的工具人

平时,“工具人"这个词,常常被用来进行一种幽默而略带自嘲的表达。我们都可能有过这样的经历,面对一堆琐碎而杂乱的任务,无奈地笑称自己为"工具人”。然而,为什么这个词会在这里被提出来?为了我们在后面编程过程中,对使用AI大模型有个形象的认知:它是一个具备操作工具能力的人工智能模型

其实,你们可能已经猜到了,我要讲的正是AI大模型中的比较最常见的使用结构:Agent + Tool。

在各种新兴的框架驱动下(比如LangChain等),人工智能不再只是我们口中的“工具”,而是变身成为一个真正意义上的“工具人”。他们是真正会使用工具的行动者,而非工具本身。他们不仅能接受和执行我们的指令,更能够熟练运用各种工具去解决问题,创造价值。

“Agent"这个词在计算机科学中有着悠久的历史。在早期,它被用于形容一种代理或者媒介,承担着在网络世界与现实世界之间建立桥梁的重任。以浏览器的"User Agent"为例,它实际上是浏览器在与网络服务器进行交互时,声明自己身份的一种方式。而在这个过程中,浏览器可以被看作是用户(User)和互联网内容(Web content)之间的"Agent”。

随着人工智能技术的快速发展,"Agent"这个词的含义也在逐渐丰富和深化。在AI领域,"Agent"常常被用来形容那些可以自主进行决策,响应环境变化,并对各种指令进行执行的模型或系统。

了解完Agent + Tool 的背景之后,我们下面切入正题看看LangChain中的Agent和Tool是如何运作的。

LangChain 下的 Agent + Tool 实践

简单实践

下面贴一个最简单常见的例子:查看机器的运行时长。

import os
from subprocess import Popen, PIPE
from langchain.llms import OpenAI
from langchain.tools import StructuredTool
from langchain.agents import initialize_agent, AgentType

def ssh(command:str, host: str, username: str = "root") -> str:
    """A tool that can connect to a remote server and execute commands to retrieve returned content."""
    return os.popen(f"ssh {host} -l{username} '{command}'").read()

agent = initialize_agent(
    [StructuredTool.from_function(ssh)], 
    OpenAI(temperature=0), 
    agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION, 
    verbose=True
)
agent.run("帮我看一下 8.130.35.2 这台机器运行多久了")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

在langchain中,提供了tools相关的工具可以非常轻松地将一个函数转换成tool,然后在初始化agent的时候,将这个工具引入:这个机器人(Agent)拿上工具(Tool)之后,变成了一个工具人。

然后我们来观察一下这个工具人是如何思考并使用工具的:

> Entering new AgentExecutor chain...

Action:
```
{
  "action": "ssh",
  "action_input": {
    "command": "uptime",
    "host": "8.130.35.2"
  }
}
```

Observation:  15:48:44 up 25 days, 41 min,  0 users,  load average: 1.04, 1.48, 2.20

Thought: I have the answer
Action:
```
{
  "action": "Final Answer",
  "action_input": "This machine has been running for 25 days and 41 minutes."
}
```

> Finished chain.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  1. Agent通过问题来思考,它需要一个工具能够查看 8.130.35.2 这台机器的运行时长。
  2. Agent盘点了他的工具之后,发现他有一个ssh工具,这个工具可能能帮助他获取机器的运行时长。
  3. 在大语言模型的训练数据中,有有关ssh和uptime的只是,所以Agent能够判断出在ssh工具中执行uptime能够获取到他需要的信息。
  4. Agent调用ssh工具传入uptime命令,获取到了这台目标机器的运行时长信息。
  5. Agent理解了一下这些信息,去除了load等无关信息,并组织了一下语言,进行了输出返回。

看了这个例子很多就会好奇,原本大语言模型不就是只能对对话,像纸上谈兵一样吗?怎么一下子就能做怎么多事情?ssh上去既然能执行uptime,那是不是执行rm -rf /也是一句话的事情?

非常好,我们带着这些问题继续往下看,看来看看一个大语言模型是如何被安上“四肢”的:

上面这个例子总共调用了2次大语言模型的接口,我们分别将两次提示词(prompt)给提取了出来:

第一次提示词

System: Respond to the human as helpfully and accurately as possible. You have access to the following tools:

ssh: ssh(command: str, host: str, username: str = 'root') -> str - A tool that can connect to a remote server and execute commands to retrieve returned content., args: {{'command': {{'title': 'Command', 'type': 'string'}}, 'host': {{'title': 'Host', 'type': 'string'}}, 'username': {{'title': 'Username', 'default': 'root', 'type': 'string'}}}}

Use a json blob to specify a tool by providing an action key (tool name) and an action_input key (tool input).

Valid "action" values: "Final Answer" or ssh

Provide only ONE action per $JSON_BLOB, as shown:

```
{
  "action": $TOOL_NAME,
  "action_input": $INPUT
}
```

Follow this format:

Question: input question to answer
Thought: consider previous and subsequent steps
Action:
```
$JSON_BLOB
```
Observation: action result
... (repeat Thought/Action/Observation N times)
Thought: I know what to respond
Action:
```
{
  "action": "Final Answer",
  "action_input": "Final response to human"
}
```

Begin! Reminder to ALWAYS respond with a valid json blob of a single action. Use tools if necessary. Respond directly if appropriate. Format is Action:```$JSON_BLOB```then Observation:.
Thought:
Human: 帮我看一下 8.130.35.2 这台机器运行多久了
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

第一次调用返回

Action:
```
{
  "action": "ssh",
  "action_input": {
    "command": "uptime",
    "host": "8.130.35.2"
  }
}
```
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

第二次提示词

System: Respond to the human as helpfully and accurately as possible. You have access to the following tools:

ssh: ssh(command: str, host: str, username: str = 'root') -> str - A tool that can connect to a remote server and execute commands to retrieve returned content., args: {{'command': {{'title': 'Command', 'type': 'string'}}, 'host': {{'title': 'Host', 'type': 'string'}}, 'username': {{'title': 'Username', 'default': 'root', 'type': 'string'}}}}

Use a json blob to specify a tool by providing an action key (tool name) and an action_input key (tool input).

Valid "action" values: "Final Answer" or ssh

Provide only ONE action per $JSON_BLOB, as shown:

```
{
  "action": $TOOL_NAME,
  "action_input": $INPUT
}
```

Follow this format:

Question: input question to answer
Thought: consider previous and subsequent steps
Action:
```
$JSON_BLOB
```
Observation: action result
... (repeat Thought/Action/Observation N times)
Thought: I know what to respond
Action:
```
{
  "action": "Final Answer",
  "action_input": "Final response to human"
}
```

Begin! Reminder to ALWAYS respond with a valid json blob of a single action. Use tools if necessary. Respond directly if appropriate. Format is Action:```$JSON_BLOB```then Observation:.
Thought:
Human: 帮我看一下 8.130.35.2 这台机器运行多久了

This was your previous work (but I haven't seen any of it! I only see what you return as final answer):

Action:
```
{
  "action": "ssh",
  "action_input": {
    "command": "uptime",
    "host": "8.130.35.2"
  }
}
```


Observation:  16:38:18 up 25 days,  1:30,  0 users,  load average: 0.81, 0.79, 1.06

Thought:
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57

第二次调用返回

I can provide the human with the uptime of the machine
Action:
```
{
  "action": "Final Answer",
  "action_input": "The machine has been running for 25 days, 1 hour and 30 minutes."
}
```
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  1. 通过提取出来的提示词,我们可以看到我们教会了大模型用一种固定的格式去进行沟通,同时限定了他的返回必须是 Valid "action" values: "Final Answer" or ssh,也就说要么直接给答案,要么寻求工具的帮助。
  2. 在第一次调用之后,大模型无法推理出结果,于是返回 action: ssh,这样框架层就能知道这是要去调用哪个工具了,于是就把参数传给那个工具,并将执行之后的结果返回 追加到第二次的调用的尾部。
  3. 于是在第二次调用的时候,在大模型的视角来看,就是他请求调用ssh,然后我们真的将调用结果返回给他,于是他能够有充分的信息做推理,就给出了Final Answer,告诉了我们这台机器运行了25天+。
  4. 有兴趣的同学可以将上面的调用直接贴进ChatGPT对话框模拟一下,看看是不是调用的返回就是这些,同时也可以试试其他的LLM是否具有这样交互的能力(针对不同的LLM需要制作不同的PromptTemplate在本段暂不展开)。

看到这里有些同学可能会有点小问题,为什么我们每次调用都是要把全量的数据全部推一遍?我们平时用大模型交谈的时候不是有上下文的吗?其实这是个工程问题:

大语言模型原本就只能做单次的文本的预测,如何实现拟人的交谈呢?在每次对话前,把前面的几轮对话数据也都输入进去,这样就会让人感觉他记得前面的内容–随着对话越来越长,如果每轮对话数据都放进去,模型扛不住了怎么办?那就从最老的删掉一些。这样是不是就和人的遗忘很像了?当聊到第三个话题的时候,可能已经忘了第一个话题聊的是啥。

我们当前使用LLM接口调用的时候,每次都是一个全新的文本预测,所以我们需要每次把全量的数据都推下去。

初识ReAct流程

看到这里,有些同学会有疑问,为什么我自己用提示词(Prompt)去问就没法形成这样的调用工具式的交互,为什么用了框架就能达到这个效果。这就是ReAct,ReAct是Reasoning and Acting缩写,意思是大模型可以根据逻辑推理(Reason),构建完整系列行动(Act),从而达成期望目标。ReAct方式的关键就是协调大语言模型和外部的信息获取,与其他功能交互:大模型是大脑,通过ReAct框架可以让大脑来控制手和脚。

1689221533534-75730dba-ac4d-4866-a213-c66ea4977e81.png

在ReAct流程中,我们可以抓住三个关键的元素:

思考(Thought): 思考是由大模型创建的,为其行为和决定提供理论支撑。我们可以通过分析大模型的思考过程,来评估其即将采取的行动是否符合逻辑。它作为一个关键指标,能够帮助我们判断其决策的合理性。相比于人类的决策,Thought的存在赋予了大模型更出色的可解释性和可信度。

行动(Act): 行动代表大模型认为需要采取的具体行为。行动一般由两个部分构成:动作和目标,这在编程中对应着API名称和其输入参数。大模型的一大优点在于,它可以根据思考的结果,选择合适的API并生成所需的参数。这确保了ReAct框架在执行方面的实用性。

观察(Obs): 观察代表大模型如何获取外部输入。它就像大模型的感知系统,将环境的反馈信息同步给大模型,帮助它进一步进行分析或者决策。

既然这个ReAct流程这么厉害,能不能实现一些更复杂的思考呢?下面就是一个复杂的思维链的例子。

思维链实践

这是一个抓取博客rss订阅然后分析的例子


from typing import Dict
import sys
import traceback
from io import StringIO
from contextlib import redirect_stdout, redirect_stderr

from langchain.llms import OpenAI
from langchain.tools import StructuredTool
from langchain.agents import initialize_agent, AgentType


def python(code_str: str, return_context: bool=False) -> Dict:
    """This is a Python execution tool. You can input a piece of Python code, and it will return the corresponding execution results. By default, it returns the first 1000 characters of both stdout and stderr. It's recommended to use the print() function to directly display the results."""
    # 为输出和错误创建StringIO对象,以便我们可以捕获它们
    stdout = StringIO()
    stderr = StringIO()
    return_head = 1000

    context = {}

    try:
        # 重定向stdout和stderr,执行代码
        with redirect_stdout(stdout), redirect_stderr(stderr):
            exec(code_str, context)
    except Exception:
        stderr.write(traceback.format_exc())

    # 获取执行后的stdout, stderr和context
    stdout_value = stdout.getvalue()[0:return_head]
    stderr_value = stderr.getvalue()[0:return_head]

    if return_context == True:
        return {"stdout": stdout_value, "stderr": stderr_value, "context": context}
    else:
        return {"stdout": stdout_value, "stderr": stderr_value, "context": {}}


agent = initialize_agent([StructuredTool.from_function(python)], OpenAI(temperature=0.3), agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
resp = agent.run("帮我利用工具总结一下 https://elastic.aiops.work/blog/feed 这个xml的博客订最近有什么更新")
print("=== resp ===")
print(resp)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

我们并不提供给大模型任何抓取工具,我们直接给他一个python的执行器,看看他能不能把我们要的东西抓下来,我们来看一下执行过程:

> Entering new AgentExecutor chain...
Thought: The user is asking for a summary of updates from an XML blog feed. I will need to fetch the XML from the provided URL and parse it to extract the relevant information. Python has several libraries that can help with this, such as 'requests' for fetching the data and 'xml.etree.ElementTree' for parsing the XML. I'll write a Python script to do this and use the execute_python tool to run it.

Action:
{
  "action": "python",
  "action_input": {
    "code_str": "import requests\nfrom xml.etree import ElementTree as ET\n\n# Fetch the XML data\nresponse = requests.get('https://elastic.aiops.work/blog/feed')\n\n# Parse the XML\nroot = ET.fromstring(response.content)\n\n# Extract the relevant information\nupdates = [{'title': item.find('title').text, 'link': item.find('link').text, 'pubDate': item.find('pubDate').text} for item in root.findall('.//item')]\n\n# Print the updates\nfor update in updates:\n    print(f"Title: {update['title']}\nLink: {update['link']}\nPublished Date: {update['pubDate']}\n")",
    "return_context": false
  }
}

Observation: {'stdout': '', 'stderr': 'Traceback (most recent call last):\n  File "/Users/****/test.py", line 22, in python\n    exec(code_str, context)\n  File "<string>", line 15\n    print(f"Title: {update['title']}\n^\nSyntaxError: EOL while scanning string literal\n', 'context': {}}
Thought:The Python code failed to execute due to a SyntaxError. It seems like the print statement in the code is not properly formatted. I need to correct the print statement and try again.

Action:
{
  "action": "python",
  "action_input": {
    "code_str": "import requests\nfrom xml.etree import ElementTree as ET\n\n# Fetch the XML data\nresponse = requests.get('https://elastic.aiops.work/blog/feed')\n\n# Parse the XML\nroot = ET.fromstring(response.content)\n\n# Extract the relevant information\nupdates = [{'title': item.find('title').text, 'link': item.find('link').text, 'pubDate': item.find('pubDate').text} for item in root.findall('.//item')]\n\n# Print the updates\nfor update in updates:\n    print(f"Title: {update['title']}\nLink: {update['link']}\nPublished Date: {update['pubDate']}\n")",
    "return_context": false
  }
}

Observation: {'stdout': 'Title: Being one of the only woman in her computer science program didn’t stop Hannah Mudge from pursuing her dream of becoming a software engineer\nLink: https://www.elastic.co/blog/culture-hannah-mudge-dream-of-becoming-software-engineer\nPublished Date: Fri, 07 Jul 2023 00:00:00 GMT\n\nTitle: Accelerating R&D in pharma with Elasticsearch, ESRE, LLMs, and LangChain — Part 1\nLink: https://www.elastic.co/blog/research-development-pharma-elasticsearch-esre-llms-langchain-1\nPublished Date: Fri, 07 Jul 2023 00:00:00 GMT\n\nTitle: Being one of the only woman in her computer science program didn’t stop Hannah Mudge from pursuing her dream of becoming a software engineer\nLink: https://www.elastic.co/blog/culture-hannah-mudge-dream-of-becoming-software-engineer\nPublished Date: Fri, 07 Jul 2023 00:00:00 GMT\n\nTitle: Accelerating R&D in pharma with Elasticsearch, ESRE, LLMs, and LangChain — Part 1\nLink: https://www.elastic.co/blog/research-development-pharma-elasticsearch-esre-llms-langchain-1\nPublish', 'stderr': '', 'context': {}}
Thought:The Python code executed successfully and returned the recent updates from the XML blog feed. The updates include the title, link, and published date of the blog posts. I will now format this information into a readable format for the user.

Action:
{
  "action": "Final Answer",
  "action_input": "Here are the recent updates from the blog:\n\n1. Title: Being one of the only woman in her computer science program didn’t stop Hannah Mudge from pursuing her dream of becoming a software engineer\nLink: https://www.elastic.co/blog/culture-hannah-mudge-dream-of-becoming-software-engineer\nPublished Date: Fri, 07 Jul 2023 00:00:00 GMT\n\n2. Title: Accelerating R&D in pharma with Elasticsearch, ESRE, LLMs, and LangChain — Part 1\nLink: https://www.elastic.co/blog/research-development-pharma-elasticsearch-esre-llms-langchain-1\nPublished Date: Fri, 07 Jul 2023 00:00:00 GMT"
}


> Finished chain.
=== resp ===
Here are the recent updates from the blog:

1. Title: Being one of the only woman in her computer science program didn’t stop Hannah Mudge from pursuing her dream of becoming a software engineer
Link: https://www.elastic.co/blog/culture-hannah-mudge-dream-of-becoming-software-engineer
Published Date: Fri, 07 Jul 2023 00:00:00 GMT

2. Title: Accelerating R&D in pharma with Elasticsearch, ESRE, LLMs, and LangChain — Part 1
Link: https://www.elastic.co/blog/research-development-pharma-elasticsearch-esre-llms-langchain-1
Published Date: Fri, 07 Jul 2023 00:00:00 GMT
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  1. 大模型上来就写了一段Python脚本,通过requests获取内容,然后通过xml来进行解析,别问我他为什么连正文都不用看,直接知道字段。你问问你手边的大模型他知不知道rss xml的格式。只是我们平时不常接触这些,所以肯定要先抓取内容过来看看,然后才能知道要取哪些字段。
  2. 很可惜原本第一把就能跑通,但是他忘了python下普通引号包裹的字符串是不能换行的,于是报错了。这个时候思维链出现了,他根据返回结果总结是print的地方报错了,所以他需要调整print部分的代码。这个就有点惊艳了,如果我写完第一段代码碰到这个报错,第一时间还不一定反应过来,可能会以为是字段搞错之类的。

The Python code failed to execute due to a SyntaxError. It seems like the print statement in the code is not properly formatted. I need to correct the print statement and try again.

  1. 第二次他就立马就改对了,print写在了同一行,我把大模型生成的两次代码都贴一下,大家可以比较一下:
# 第一次代码

import requests
from xml.etree import ElementTree as ET

# Fetch the XML data
response = requests.get('https://elastic.aiops.work/blog/feed')

# Parse the XML
root = ET.fromstring(response.content)

# Extract the relevant information
updates = [{'title': item.find('title').text, 'link': item.find('link').text, 'pubDate': item.find('pubDate').text} for item in root.findall('.//item')]

# Print the updates
for update in updates:
    print(f"Title: {update['title']}
Link: {update['link']}
Published Date: {update['pubDate']}
")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
# 第二次代码
import requests
from xml.etree import ElementTree as ET

# Fetch the XML data
response = requests.get('https://elastic.aiops.work/blog/feed')

# Parse the XML
root = ET.fromstring(response.content)

# Extract the relevant information
updates = [{'title': item.find('title').text, 'link': item.find('link').text, 'pubDate': item.find('pubDate').text} for item in root.findall('.//item')]

# Print the updates
for update in updates:
    print(f"Title: {update['title']}\nLink: {update['link']}\nPublished Date: {update['pubDate']}\n")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

通过第二个例子,我们可以看到,当大模型调用工具报错的时候,他会用自己的已有知识对报错信息做分析,调整修改代码,直到跑成功。由此看来,这个“工具人”不仅仅是有工具,他更是具备人的推理和反思能力,能够通过多次尝试将问题解决。

看到这里,是不是有些同学已经跃跃欲试,想要去试两把了,下面我们来讲讲在普通的编程中如何融合Agent 和 Tool。

普通编程模型如何融合 Agent + Tool

由MapReduce编程范式引发思考

在分布式计算领域,通过MapReduce这样的编程范式,让分布式计算变得简单,不需要了解分布式通信同步原理,写完mapper和reducer,就能在上千台服务器的集群上运行程序,还不用担心出现机器故障等各问题,而今mapper和reducer也不用写了,一个SELECT SQL下去就会自动被拆成若干个mapper和reducer去运行。

事实上,MapReduce这样的编程范式,非常像一个大公司的工作结构。工作首先会被分解并分配到许多小组(Mapper),这些小组相当于公司中的一线员工,他们是执行具体任务的人,每个小组都在各自的小领域内专注于自己的工作,处理他们被分配到的数据或任务。

每个人并不需要关心其他部门或者其他生产环节的事情,只需集中注意力完成自己的工作就好。当他们完成他们的任务之后,这些小组或者员工会将他们的成果提交给他们的经理(Reducer)。这些经理会收集和汇总所有的结果,形成一个更高层次的报告或决策。最后,高层(还是Reducer)会根据这些汇总的数据,做出最终的决策或报告。这就像公司的CEO或董事会基于从各个部门收集来的信息,做出全公司范围的决策。

这时候,再让我们看看我们上文制作出来“工具人”,是不是就可以直接被安排进MapReduce模型里面? 乃至于这个“工具人”如果放在高层成为决策层,也没什么大问题?毕竟这个“工具人”的经验和知识可能都远超一些职业经理人。

同时,我们还需要考虑一点,MapReduce结构不一定是复杂工程结构的最优解,之前为什么这样设计是因为调度器都没那么智能。而以后如果调度器本身就是一个大模型,他是否会根据业务场景,编排出比MapReduce更优的结构呢?所以这个甚至都不一定是MapReduce了?是否甚至会糅合进一些社会学的结构进去?我们可以称之为“AI大模型社会学计算架构”?进而我们需要讨论多个AI大模型之间如何交流才能更好地协作解决问题?

针对AI大模型的复杂工程脑洞,我们可以让子弹再飞一会儿。回到当前我们的编程模型中,看看这样的工程结构要如何落地。

装饰器模式

在常见的编程中,装饰器可能一种用得比较多的设计模式,这种模式可以提高代码的可读性和可维护性,并有助于实现关注点的分离。例如在Python的Flask框架中,一个基本的路由可能会看起来像这样:

@app.route('/hello')
def hello_world():
    return 'Hello, World!'
  • 1
  • 2
  • 3

那么 Agent 和 Tool 是不是也可以用装饰器,进入到我们编程中,是的,langchain也提供了 @tool 这样的装饰器,但总感觉好像还是无法编写出上文我们想的那种工程结构。如果只是装饰器,看起来也不复杂,我们可以设计一个简单的装饰器实现:

  • @tool() 与langchain的相同,在一个函数外包了之后,就可以把一个函数转变成tool
  • @agent(tools=[...], llm=..., ...) 在一个函数外包了之后,这个函数会变成一个agent,函数执行输出就会变成agent.run(...) 中填入的提示词。其本身又是一个函数,能够被当做tool被其他agent来使用。

单这样说有点抽象,我们来看一个例子。

这个例子不像前面例子这么简单,大致交代一下背景:在云原生场景下,工作负载之间的互相依赖常常使用service,而在排查问题的时候,我们常常会去找这个Pod关联的Service。这里例子就是让大模型去帮我们寻找这些service。

import os
import sys
from subprocess import Popen, PIPE

sys.path.insert(0, os.path.split(os.path.realpath(__file__))[0] + "/../../")

from aibond import AI
from langchain import OpenAI

ai = AI()


def popen(command):
    child = Popen(command, stdin = PIPE, stdout = PIPE, stderr = PIPE, shell = True)
    out, err = child.communicate()
    ret = child.wait()
    return (ret, out.strip(), err.strip())

@ai.tool()
def k8sLabel(name: str, kind: str, namespace: str) -> str:
    """This tool can fetch the labels of Kubernetes objects."""
    cmd = "kubectl get " + kind + " " + name + " -n " + namespace + " -o jsonpath='{.metadata.labels}'"
    (ret, out, err) = popen(cmd)
    return out

@ai.tool()
def k8sServiceSelectorList(namespace: str) -> str:
    """This tool can find all services within a namespace in Kubernetes and retrieve the label selectors for each service."""
    cmd = "kubectl get svc -n " + namespace + "  -o jsonpath="{range .items[*]}{@.metadata.name}:{@.spec.selector}{'\n'}{end}""
    (ret, out, err) = popen(cmd)
    return out

@ai.agent(tools=["k8sLabel", "k8sServiceSelectorList"], llm=OpenAI(temperature=0.2), verbose=True)
def k8sPodServiceFinder(name: str, namespace: str) -> str:
    """This tool can find the services associated with a Kubernetes pod resource."""
    return f"帮我列出 {namespace} 这个ns下所有的service,在这个service list中找出与 pod {name} 的label相关的service,返回的结果只有service的名称即可"


a = ai.run("使用所有的工具去查找sreworks这个ns下 prod-health-health-6cbc46567-s6dqp 这个pod的关联的k8s资源", llm=OpenAI(temperature=0.2), agents=["k8sPodServiceFinder"], verbose=True)
print(a)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 最终当我们想要解决问题的时候,依赖 k8sPodServiceFinder 这个agent来进行帮助。
  • k8sPodServiceFinder本身也是agent,他将 name 和 namespace 这两个参数转换成一个prompt,同时他依赖 k8sLabel 和 k8sServiceSelectorList 这两个tool 去解决问题,在prompt中教会了大模型如何去使用这两个工具
  • 最终整个运行的层级结构如下图所示:

截屏2023-07-24 10.21.06.png

因为我们使用了修饰器来进行编程,事实上这个层级结构再复杂一点也能非常轻松地应对。整体使用下来,我们需要考虑如下几个点:

  1. 在多层的结构中,可以混合使用多种大模型,但大模型调用次数会增多,整体的运行速度也会变慢。
  2. 在编程中引入了大模型之后,几乎不用写if-else等控制流了,只需要一些原子性的工具,大模型就会自动将这些工具串起来。
  3. Agent + Tool的结构可以类比 ChatGPT 的插件系统,不过这是个私有的插件系统,用户可以自由定制插件。
  4. 之前写代码时候常常会说“优雅的接口,丑陋的实现”,现在这个丑陋的实现现在似乎可以直接变成一段prompt。那么如果要验证某个功能,是不是可以直接放一堆prompt上去调通原型,后面再慢慢挨个替换接口?

上面的这些装饰器是一些语法糖组成的框架,核心部分是langchain,有兴趣想动手实践的同学可以参考框架代码 https://github.com/alibaba/sreworks-ext/tree/master/aibond

面向对象的AI编程

将类对象转换成tool

在有了上文提到的框架,我们就可以做很多AI相关的编程尝试,但是总发现只能编写简单功能,没法特别复杂,为什么呢?因为只有函数能变成tool,而我们平时编程的时候,大多数情况都是面向对象的编程。一下子变成了到处用裸函数,自然会有些不适应。那么问题来了,为什么不能把一个类对象变成一个tool呢?函数是无状态的,类对象是要实例化的,是有状态的。但如我们前面在分析prompt时所提到,我们每次调用都会全量推信息,那么这些信息不就是有状态的吗?包含个类实例数据应该不在话下吧?

于是我们可以设置这样一个带状态的function,像个代理一样,把一个类对象包在里面:

def demo_class_tool(func: str, args: Dict, instance_id: str = None) -> Dict:
    """
    This is a tool that requires instantiation. You can first call the '__init__' function to instantiate, this call will return an 'instance_id'. Subsequently, you can use this 'instance_id' to continue operating on this instance.
    Below are the available funcs for this tool:
    - func: __init__  args: {{'url': {{'title': 'Url', 'type': 'string'}} }}
    - func: read  args: {{'limit': {{'title': 'Limit', 'type': 'intger', 'default': '1000'}}

"""
    ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 在函数的描述中就说明这个工具的使用方式,里面包含了哪些含函数(就是类对象下有多少个函数)
  • 引导大模型先去调用 __init__拿到实例化后的 instance_id
  • 然后引导大模型拿着 instance_id 并且参考我们在描述中的参数来调用函数,就实现类调用。

还是最前面的一个例子查看机器运行时长的例子,我们可以使用类对象再实现一遍:

import os
import sys
import paramiko

sys.path.insert(0, os.path.split(os.path.realpath(__file__))[0] + "/../../")

from aibond import AI
from tools.Linux.SSH import SshClient
from langchain import OpenAI

ai = AI()

class SshClient():
    """A tool that can connect to a remote server and execute commands to retrieve returned content."""
    _client = None
    def __init__(self, host: str, username: str = "root", password: str = None):
        self._client = paramiko.SSHClient()
        self._client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        self._client.connect(host, username=username, password=password)

    def exec_command(self, command: str) -> Dict:
        stdin, stdout, stderr = self._client.exec_command(command)
        retcode  = stdout.channel.recv_exit_status()
        output_stdout = stdout.read().decode('utf-8')
        output_stderr = stderr.read().decode('utf-8')

        stdin = None
        stdout = None
        stderr = None

        return {"stdout": output_stdout, "stderr": output_stderr, "exitStatus": retcode}


resp = ai.run("帮我看看 8.130.35.2 这台机器启动了多久了", llm=OpenAI(temperature=0.2), tools=[SshClient], verbose=True)
print("=== resp ===")
print(resp)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 我们可以看到,我们引入了一个client来进行ssh,我们把原本带着机器IP执行命令化成了两步,先实例化sshClient,然后调用sshClient.exec_command(...)来执行命令。我们来看看大模型能否理解。
> Entering new AgentExecutor chain...
Action:
```
{
  "action": "SshClient",
  "action_input": {
    "sub_func": "__init__",
    "sub_args": {
      "host": "8.130.35.2",
      "username": "root",
      "password": ""
    }
  }
}
```


Observation: {'instance_id': 'cbbb660c0bc3'}
Thought: I need to use the instance_id to execute a command
Action:
```
{
  "action": "SshClient",
  "action_input": {
    "sub_func": "exec_command",
    "sub_args": {
      "command": "uptime"
    },
    "instance_id": "cbbb660c0bc3"
  }
}
```


Observation: {'stdout': ' 23:18:55 up 25 days,  8:11,  0 users,  load average: 0.29, 0.55, 0.84\n', 'stderr': '', 'exitStatus': 0}
Thought: I have the answer
Action:
```
{
  "action": "Final Answer",
  "action_input": "This machine has been up for 25 days, 8 hours, and 11 minutes."
}
```

> Finished chain.
=== resp ===
This machine has been up for 25 days, 8 hours, and 11 minutes.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

我们可以看到,大模型很好地理解了这个实例化的过程,他会先用ip地址去做实例化,然后再执行命令。

如果一个类对象能变成tool,AI编程就没那么费劲了,甚至于有些类对象是不是都用做什么处理,直接转成tool给Agent用就行了。以及大数据的处理是不是也可以采用这个方案:先把大数据载入到一个对象中,然后提供出若干个方法让大模型慢慢去分析或消化这些数据?大文本的汇总是否也可以采用这个思路?

有关类对象tool调用的功能实现,已经推送到了框架中,欢迎大家试用 https://github.com/alibaba/sreworks-ext/blob/master/aibond/aibond/core.py

有关类对象tool化之后的更多探索,会在第二篇中展开。

真正的对象

在编程的早期学习阶段,面向对象编程(OOP)的概念对于初学者而言,是一种全新而又复杂的概念。为了帮助初学者理解,我们常常采取了形象化的教学手段,如用“手”和“脚”来举例说明一个类(class)的概念。当时懵懂的我,以为只要给每个class配完手脚,整个程序就会活起来,但是代码写多了发现好像离这个“活”总有点差距。因为我们在编写类时,常常把它们视为一个静态的存储。而真正的对象,它们应该具备更多的主动性和动态性。它们应该能主动进行交流(“讲话”),能够执行任务(“干活”)。这就像在现实世界中,每个个体都是一个独立的、能够独立思考和行动的实体。

是的,如前文所说,我们的“工具人”就能实现这一点。上文的例子让我们看到了一个类对象也能变成一个tool,供agent使用。那么在这个类对象,里面我们是不是能嵌入大模型驱动,让其真正地活起来?使得 agent 调用 agent 的时候,不仅只是要一个结果,更像是一种面向结果的沟通?

这些探索也会在后续的篇章展开。

参考
●《ReAct: Synergizing Reasoning and Acting in Language Models》https://arxiv.org/abs/2210.03629
●《aibond使用案例》https://github.com/alibaba/sreworks-ext/blob/master/aibond/cases/README.md

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/311372
推荐阅读
相关标签
  

闽ICP备14008679号