我是一个业余机器人爱好者,今天想谈一下我是怎么开发OriginBot的。

其实从标题也能知道,我是利用ChatGPT结伴开发的,但是这个过程中有很多细节,想要详细的说一下的。我想,这个过程应该会对其他人也有有一点帮助。

在本地部署一个ChatGPT

1. 部署ChatGPT

众所周知,OPENAI在国内不能直接访问,而且GPT4要付费才能使用,20美金一个月,3个小时内只能调用25次GPT4。
所以我其实使用的是Azure OPENAI服务,最大的好处就是没有网络限制,国内可以直接访问。

我用的Azure是公司的账户,所以不用自己管理账户和申请Azure OPENAI服务,相关的操作网上有很多资料,我贴两个我觉得写的还不错的博客链接:

  1. Azure OpenAI Service 注册申请与配置心得
  2. 如何申请Azure OPENAI

Azure自己的OPENAI服务没有历史聊天记录功能,也就是说,每次刷新页面后,之前的聊天记录都没了,这个非常不方便,因为经常需要翻看之前的记录或者代码之类的,所以我基于网上的两个开源项目自己搭建了一个本地的ChatGPT。

  1. azure-openai-proxy
    这个项目的作用是对Azure OPENAI服务的API进行封装,使其跟openai原生的API一致
  2. https://github.com/Yidadaa/ChatGPT-Next-Web
    这个项目的作用是在本地起一个类似chatgpt官网那样的网站,可以让我们在本地跟chatgpt聊天、提问

两个开源项目的文档非常完善,有兴趣的可以去仔细看一下,我在这里提供两行命令,修改一下参数直接运行就能使用了。

# 拉取镜像
docker pull ishadows/azure-openai-proxy:latest
# 运行一个azure-openai-proxy的容器
docker run -d -p 8080:8080 --name=azure-openai-proxy --env AZURE_OPENAI_ENDPOINT=https://xxxxxxxxx.openai.azure.com/ --env AZURE_OPENAI_MODEL_MAPPER=gpt-35-turbo-0301=xxxxxxx,gpt-4=xxxxxx,gpt-4-32k=xxxxx ishadows/azure-openai-proxy:latest
# 拉镜像
docker pull yidadaa/chatgpt-next-web:latest
# 运行一个chatgpt-next-web的容器
docker run -d -p 3000:3000 -e OPENAI_API_KEY="xxxxx" -e CODE="xxxxx" -e BASE_URL=http://ip:8080 yidadaa/chatgpt-next-web:latest

几个参数说明如下:

  • AZURE_OPENAI_ENDPOINT=https://xxxxxxxxx.openai.azure.com
    这个是自己部署的模型的https地址
  • AZURE_OPENAI_MODEL_MAPPER=gpt-35-turbo-0301=xxxxxxx
    这是Azure中OPENAI几个模型的匹配关系,gpt-35-turbo-0301 是Azure官方定义好的,后面的xxxxx应该填写自己定义的名字,可以填写多个,用逗号隔开,如命令行中所示
  • OPENAI_API_KEY=”xxxxx”
    这个就是Azure OPENAI的秘钥,
  • CODE=”xxxxx”
    这是用于在页面上做认证的,防止你部署的chatgpt-next-web被别人访问,建议这个一定要足够长,毕竟一旦被别人访问到,消耗的是自己的钱

上面几个参数配置成功之后,就可以在本地浏览器访问http://127.0.0.1:3000 端口,效果如下:

2. 参数调整

参数调整只有一个参数需要注意,就是Temperature,经常使用ChatGPT的人应该知道这个参数,我不是专业的大模型从业者,所以不想解释这个参数的作用原理,只说一下我的实际使用感受。

Temperature的范围是0~1,值越大,回答越多样性,但是错误也更多,值越小,回答越固定,但是也更可靠。

我自己在OriginBot这个场景下使用的值是0.4,因为我还是希望它更靠谱一点,不要经常出错,因为目前在跟机器人相关的更多场景下,我还没有自己判断它的回答正确与否的能力,只能选择相信它。
但是对于比较专业的人,这个值可以设置的高一点,这样可以获得更加多样性的回答,不过要注意甄别它回答的答案。

如何编写Prompt和如何提问

按照前面的步骤,我们现在有了一个可用的ChatGPT了,现在开始使用它。

使用ChatGPT的时候主要需要注意的就是Prompt和提问两块,我使用的Prompt分为通用Prompt和定制Prompt两部分,下面会按照共用Prompt、提问和定制Prompt的顺序来介绍。

1. 通用Prompt

以下是我使用的Prompt,简单来说就是一个角色赋予,给chatgpt指定一个角色,比如这里的“机器人开发专家”, 然后描述一些对这个角色的期待,比如我希望它熟悉ros2和自动导航,另外,因为我对python比较熟悉,希望它尽量使用Python而不是C++。

你是一名机器人开发专家,熟悉机器人行业的最新技术和现状,熟练掌握机器人开发的必要技能,例如ros2,嵌入式开发、STM32芯片、PCB知识、SLAM、自动导航和驾驶、机器人运动模型等,我会交给你一些任务,请你尽力完成,在完成任务的过程中,你应该结合你的岗位和技能,尽可能做到完善,并且你可以做出你觉得必要的修改和延伸,另外,如果交给你的任务需要写代码,尽量使用Python

2. 如何提问

关于提问,最简单的方法,肯定就是直接跟chatgpt聊天,但是这样效果一般。我的做法是把OriginBot的代码喂给chatgpt,然后再提问题,这样效果会比较好。

我写了一个脚本,可以把本地的代码生成格式化的文件,我们可以生成的文件的内容发送给chatgpt,脚本如下:

import os


def is_code_file(file_path):
    extensions = [
        '.py',
        '.cpp',
        '.c',
        '.h',
        '.java',
        '.js',
        '.css',
        '.html',
        '.php',
        '.rb',
        '.rs',
        '.md',
        '.txt',
        '.yaml',
        '.yml',
        '.lua',
    ]
    return any(file_path.endswith(ext) for ext in extensions)


def save_structure_and_files(directory, output_file, excluded_directories=None):
    if excluded_directories is None:
        excluded_directories = []

    with open(output_file, 'w') as outfile:
        # Save directory structure
        outfile.write("Directory Structure:\n")
        for root, dirs, files in os.walk(directory):
            if any(excluded_dir in root for excluded_dir in excluded_directories):
                continue
            level = root.replace(directory, '').count(os.sep)
            indent = ' ' * 4 * (level)
            outfile.write(
                '{}{}{}\n'.format(
                    indent, '|----' if level > 0 else '', os.path.basename(root)
                )
            )
            sub_indent = ' ' * 4 * (level + 1)
            for file_name in files:
                outfile.write('{}|----{}\n'.format(sub_indent, file_name))

        # Save code files content
        outfile.write("\n\nFiles Content:\n")
        for root, _, files in os.walk(directory):
            if any(excluded_dir in root for excluded_dir in excluded_directories):
                continue

            for file in files:
                file_path = os.path.join(root, file)
                if is_code_file(file_path):
                    outfile.write(f"\n\n{file_path}\n")
                    outfile.write('-' * len(file_path))
                    outfile.write("\n")
                    with open(file_path, 'r') as code_file:
                        content = code_file.read()
                        outfile.write(f"{content}\n")


# Usage example
directory = "/originbot/originbot_navigation"
output_file = "output.txt"
excluded_directories = ["venv", "send_goal"]
save_structure_and_files(directory, output_file, excluded_directories)

脚本的作用是对directory(/originbot/originbot_navigation)目录中所有文件生成一个目录树,然后再把每一个文件的内容复制到output.txt中。

以/originbot/originbot_navigation目录为例,效果如下:

Directory Structure:
originbot_navigation
    |----CMakeLists.txt
    |----package.xml
    |----param
        |----originbot_nav2.yaml
    |----config
        |----lds_2d.lua
        |----ekf.yaml
    |----launch
        |----occupancy_grid.launch.py
        |----cartographer.launch.py
        |----ekf.md
        |----nav_bringup.launch.py
        |----odom_ekf.launch.py
    |----maps
        |----my_map.yaml
        |----my_map.pgm


Files Content:


/home/zhixiang_pan/learningspace/origin/originbot/originbot_navigation/CMakeLists.txt
-------------------------------------------------------------------------------------
cmake_minimum_required(VERSION 3.5)
project(originbot_navigation)

# Default to C99
if(NOT CMAKE_C_STANDARD)
  set(CMAKE_C_STANDARD 99)
endif()

可以看到,一开始是一个目录树,然后就是具体的代码。

这样做的好处在于可以让ChatGPT更准确地理解OriginBot现有的代码和功能,再让它教我或者开发新的功能时候就更容易理解到我们的需求。

但是,再指定directory参数的时候,directory所指向的目录中,不要包含太多的文件,不然最后生成的output.txt的内容太多,超过了ChatGPT的token上限也不行。

我用的是gpt4-32k,也就是token的上限是32k,现在还有gpt3.5-turbo-120k的,也就是支持120k token,不过我没有使用过,不知道效果如何。

3. 定制Prompt

在“公共Prompt”里面,我提供了一个文字版的Prompt,这里我再提供另一种Prompt,如下:

    我会交给你一些信息、要求,请你完成任务。
    以下是信息:
    1. OriginBot是一个两轮的轮式机器人,基于ROS2 foxy开发
    2. 运动速度的topic是/cmd_vel
    以下是要求:
    1. 代码只能用Python
    2. 要基于rclpy.node的Node类拓展
    3. 代码中要考虑在合适的地方调用rclpy.shutdown()来销毁当前Node节点,而不是要求手动介入
    4. 不管什么场景下,OriginBot的速度不要超过0.4米/s
    5. 输出只能包含markdown格式的python代码,格式模板如下:
    ```python
    ###---
    import rclpy
    from geometry_msgs.msg import Twist
    from rclpy.node import Node


    class OriginBot(Node):
        def __init__(self):
            ...

        def xxx_function(self):
            ...

    def main(args=None):
        rclpy.init(args=args)
        originbot = OriginBot()
        rclpy.spin(originbot)
        originbot.destroy_node()
        rclpy.shutdown()

    if __name__ == '__main__':
        main()
    ###---
代码开头和结尾的###---都必须要显示的输出,并且最后要调用main函数
以下是任务:
xxxxx

```

可以很明显地看出来两种Prompt的区别。在这个Prompt里面,我会告诉ChatGPT很多具体的信息,比如OriginBot的速度的topic、限定线速度的大小、操作系统的版本等,最重要的是,提供了一个代码模板,要求ChatGPT按照这个模板回答问题。

这种Prompt适用于我们明确地知道我们“想要什么”,只是需要ChatGPT来写代码,提供代码模板可以提高ChatGPT生成的代码的准确率。

从我自己的使用经验来看,提供模板之后,对于单个且具体的任务,ChatGPT生成的代码都是可以直接使用的,不用做任何修改。所谓单个且具体的任务,比如像:让OriginBot画一个半径为0.5米的圆圈。

以上就是我使用ChatGPT结伴开发OriginBot的过程和方法,欢迎大家交流。