2024年7月

#### 前提提要:

.java文件通过java -c 生成.class文件,这部分并非是JVM需要处理的部分,JVM处理的部分是基于生成的class文件,生成的部分是由编译器来负责

一个字节码文件的主要组成部分

image-20240705134632708

使用工具说明

idea的JclassLib插件

使用步骤:

  1. 运行代码(只要你更新了代码就需要,或者build)
  2. image-20240705135213385

基础信息介绍

image-20240705134952556

魔数

魔数并非在这里展示,魔数其实有点类似一个文件的格式中的一部分,所有的软件开发中关于打开文件都会这么设计,用于识别说我们能否成功打开

java中是开头是cafebabe

其他文件如下:

image-20240705135756018

主版本号

1.2后大版本对应的公式是主版本号-44

image-20240705135921638

关于版本号可能遇到的problem及解法
  1. 版本不匹配
    1. 提高java版本
    2. 降低依赖版本

image-20240705140132316

在这之中可能还会遇到的问题:jdk版本存在bug,虽然很多人都说什么永远jdk8,但是实际使用上jdk8在性能上以及开发上可能会比jdk17多不少工作量,例如我之前尝试转为http2,而在jdk8需要引入其他依赖,而jdk9则可以直接转

常量池介绍以及属性介绍

此常量池非JVM中的字符串常量池
,仅仅是当前class文件中为了降低一定的空间占用和加快JVM解析class文件而使用的常量池而已

下列讨论也仅局限于字节码文件中的常量池,不会涉及JVM部分

在此先给常量池定论:为了节省空间而使用的池化技术

首先从字段入手:

image-20240705141818983

image-20240705142237700

Problem1:为什么aaa变量可以打开?而其他的不行?

解答:是因为他使用的是static和final同时修饰

所有以cp开头的都最终指向了常量池中的一个记录,例如aaa这个变量名,以及对应的aaa的类型string,具体内容如下:

image-20240705142003501

image-20240705142022100

我们再看看ConstantValue的信息:

image-20240705142358192

problem2:这个属性名索引为何存在且为什么要是constantValue?

首先回答一下为什么要有constantvalue,这是由于我们使用final修饰了,所以最终的效果就是有了一个指向constantValue的引用,也就是我们java,字节码层面实现我们final修饰不可改变的效果,如下图所示,而这个属性值为什么存在也不言而喻了

image-20240705144543867

最后回到正题:我们可以发现当String aaa和Stirng ccc都="aaa"时,他的常量值索引都指向7

image-20240705144724267

那么7的内容是什么?7的类型是string_info,而之中又指向了8?

image-20240705144925308

8才是真正的aaa

image-20240705145000278

那么问题来了?

为什么要这么设计?

关于字段中的constant_value为什么要指向string?然后string再指向其中的一个具体的utf8_info呢?

因为最终jvm会有一个string常量池,我们要保存string和utf8_info中保存的值的关系,好方便之后我们将其存储到常量池中

追问:那为什么不直接保存在对应的string中,也即我们string不存符号引用而是直接存对应的常量?

这样的话如果有其他变量也纸箱这里应该也没问题吧?

没错,但是例如string abc="abc",对应的变量名也要存储,而java中采用的变量名的引用就指向对应的utf_8的"abc"

那么abc直接指向这个string不行吗?

可以,不过这样的话,能够更节省一些空间复杂度,但是会消耗更多的时间,尤其对于变量之后运行中的处理

总结

常量池是把对应的我们会使用到的常量抽出来,无论是各个参数或者变量名类名等等,他的核心之一就是要复用

而字段在目前阶段只会对于final static修饰的,因为目前能够处理的也只有些final和static修饰的值,其他是不行的,也就是编译阶段能处理的也就这一些了

其他字段的赋值需要之后类的生命周期在各个阶段再进行赋值

方法介绍

image-20240705152339655

其中字节码部分就是对应方法的执行流程

而异常表主要是trycatch才会有的

而杂项对应的操作数栈的深度是字节码运行之中对于操作数栈使用的时候会使用到的栈的深度(与数据结构无异,而最大深度是因为在这之中便可以计算出来)

image-20240705152458362

局部变量最大槽数是这里:也就是方法中使用的局部变量,包括参数等等

image-20240705152926361

关于属性就不赘述了,意义不大

前言


基于debian11 + podman 搭建 nacos-server 用于简单测试。

nacos-server基于java,如果直接运行还要准备java环境,在docker/podman 镜像中发现了打包好的nacos-server镜像,集成了java和nacos-server。

正文


1. 准备mysql服务

如果有自己的mysql服务器,可以略过此步骤。
如果想要自己安装mysql,可以参考
podman安装mysql容器

2. 拉取nacos-server镜像

  1. 如果镜像源不可用,建议使用下面的镜像源:
    修改/etc/containers/registries.conf
unqualified-search-registries = ["docker.io"]
[[registry]]
prefix = "docker.io"
insecure = false
blocked = false
location = "docker.io"
[[registry.mirror]]
location = "docker.nastool.de"
  1. 拉取nacos-server镜像

搜索 nacos-server镜像

root@brian:~/nacos# podman search nacos
INDEX      NAME                                            DESCRIPTION                                      STARS   OFFICIAL  AUTOMATED
docker.io  docker.io/nacos/nacos-server                    This project contains a Docker image meant t...  472               [OK]

拉取 nacos-server 镜像

podman pull docker.io/nacos/nacos-server

3. 准备环境

  1. 创建目录
# 可自行定义
# 存放日志
mkdir -p /root/nacos/logs
# 存放配置文件,注意,这里目的只是为了查看配置文件,并不会使用这些,下面nacos-server启动时,会用传递环境变量的方式传参
mkdir -p /root/nacos/conf
  1. 初步创建(仅创建,不运行)nacos-server容器,目的是复制配置文件到宿主机
# 创建临时容器
podman create --name=tst nacos-server
# 复制配置文件
cd /root/nacos/
podman cp nacos:/home/nacos/conf - | tar xf - -C ./
ls ./conf
# 1.4.0-ipv6_support-update.sql  announcement_en-US.conf  announcement_zh-CN.conf  application.properties  console-guide.conf  derby-schema.sql  mysql-schema.sql  nacos-logback.xml
# 删除临时容器
podman rm tst

这里比较关心的是 mysql-schema.sql

/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/******************************************/
/*   表名称 = config_info                  */
/******************************************/
CREATE TABLE `config_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) DEFAULT NULL COMMENT 'group_id',
  `content` longtext NOT NULL COMMENT 'content',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  `c_desc` varchar(256) DEFAULT NULL COMMENT 'configuration description',
  `c_use` varchar(64) DEFAULT NULL COMMENT 'configuration usage',
  `effect` varchar(64) DEFAULT NULL COMMENT '配置生效的描述',
  `type` varchar(64) DEFAULT NULL COMMENT '配置的类型',
  `c_schema` text COMMENT '配置的模式',
  `encrypted_data_key` text NOT NULL COMMENT '密钥',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';

/******************************************/
/*   表名称 = config_info_aggr             */
/******************************************/
CREATE TABLE `config_info_aggr` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
  `content` longtext NOT NULL COMMENT '内容',
  `gmt_modified` datetime NOT NULL COMMENT '修改时间',
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';


/******************************************/
/*   表名称 = config_info_beta             */
/******************************************/
CREATE TABLE `config_info_beta` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL COMMENT 'content',
  `beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  `encrypted_data_key` text NOT NULL COMMENT '密钥',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';

/******************************************/
/*   表名称 = config_info_tag              */
/******************************************/
CREATE TABLE `config_info_tag` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
  `tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL COMMENT 'content',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';

/******************************************/
/*   表名称 = config_tags_relation         */
/******************************************/
CREATE TABLE `config_tags_relation` (
  `id` bigint(20) NOT NULL COMMENT 'id',
  `tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
  `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
  `nid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增长标识',
  PRIMARY KEY (`nid`),
  UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';

/******************************************/
/*   表名称 = group_capacity               */
/******************************************/
CREATE TABLE `group_capacity` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
  `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
  `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
  `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
  `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
  `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
  `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';

/******************************************/
/*   表名称 = his_config_info              */
/******************************************/
CREATE TABLE `his_config_info` (
  `id` bigint(20) unsigned NOT NULL COMMENT 'id',
  `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增标识',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL COMMENT 'content',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
  `op_type` char(10) DEFAULT NULL COMMENT 'operation type',
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  `encrypted_data_key` text NOT NULL COMMENT '密钥',
  PRIMARY KEY (`nid`),
  KEY `idx_gmt_create` (`gmt_create`),
  KEY `idx_gmt_modified` (`gmt_modified`),
  KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';


/******************************************/
/*   表名称 = tenant_capacity              */
/******************************************/
CREATE TABLE `tenant_capacity` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
  `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
  `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
  `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
  `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
  `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
  `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';


CREATE TABLE `tenant_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `kp` varchar(128) NOT NULL COMMENT 'kp',
  `tenant_id` varchar(128) default '' COMMENT 'tenant_id',
  `tenant_name` varchar(128) default '' COMMENT 'tenant_name',
  `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
  `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
  `gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
  `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';

CREATE TABLE `users` (
        `username` varchar(50) NOT NULL PRIMARY KEY COMMENT 'username',
        `password` varchar(500) NOT NULL COMMENT 'password',
        `enabled` boolean NOT NULL COMMENT 'enabled'
);

CREATE TABLE `roles` (
        `username` varchar(50) NOT NULL COMMENT 'username',
        `role` varchar(50) NOT NULL COMMENT 'role',
        UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);

CREATE TABLE `permissions` (
    `role` varchar(50) NOT NULL COMMENT 'role',
    `resource` varchar(128) NOT NULL COMMENT 'resource',
    `action` varchar(8) NOT NULL COMMENT 'action',
    UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);

INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);

INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');
  1. 在mysql中创建数据库以及表,给nacos-server使用

创建数据库 nacos (或者其他名字)

在上面创建的nacos数据库中,执行步骤2中的 mysql-schema.sql 的内容,初始化数据库表和用户数据。

4. 运行nacos

在/root/nacos/目录下配置脚本run.sh如下:

#/bin/bash
podman run -itd --rm \
        -v /root/nacos/logs:/home/nacos/logs \
        -p 8848:8848 \
        -p 9848:9848 \
        --name nacos \
        --net=host \
        -e MODE=standalone \
        -e TIME_ZONE='Asia/Shanghai' \
        -e NACOS_AUTH_ENABLE=true \
        -e NACOS_AUTH_TOKEN=SecretKeyM1Z2WDc4dnVyZkQ3NmZMZjZ3RHRwZnJjNFROdkJOemEK \
        -e NACOS_AUTH_IDENTITY_KEY=mpYGXyu7 \
        -e NACOS_AUTH_IDENTITY_VALUE=mpYGXyu7 \
        -e SPRING_DATASOURCE_PLATFORM=mysql \
        -e MYSQL_SERVICE_HOST=127.0.0.1 \
        -e MYSQL_SERVICE_PORT=3306 \
        -e MYSQL_SERVICE_DB_NAME=nacos \
        -e MYSQL_SERVICE_USER=你的mysql用户名 \
        -e MYSQL_SERVICE_PASSWORD=你的mysql密码 \
        docker.io/nacos/nacos-server:latest
  • -itd
    - 后台运行,允许使用 podman exec nacos bash 指令登录进容器,调试建议改成
    -it
    并 删除后面的
    --rm
  • -rm
    - 容器stop或者程序挂掉后直接自动删除,如果想要长时间运行不建议使用此选项
  • --net=host
    - 容器使用宿主机的网络,这样比较偷懒,但是也比较灵活,否则很容易出现nacos-server连不上外面的mysql

鉴权相关,不用的话可以去掉,这样就不用输入nacos的用户名和密码了

-e NACOS_AUTH_ENABLE=true \
-e NACOS_AUTH_TOKEN=SecretKeyM1Z2WDc4dnVyZkQ3NmZMZjZ3RHRwZnJjNFROdkJOemEK \
-e NACOS_AUTH_IDENTITY_KEY=mpYGXyu7 \
-e NACOS_AUTH_IDENTITY_VALUE=mpYGXyu7 \

数据库配置

-e MYSQL_SERVICE_HOST=127.0.0.1 \
-e MYSQL_SERVICE_PORT=3306 \
-e MYSQL_SERVICE_DB_NAME=nacos \
-e MYSQL_SERVICE_USER=你的mysql用户名 \
-e MYSQL_SERVICE_PASSWORD=你的mysql密码 \

运行:chmod +x ./run.sh && ./run.sh

此外,关于容器相关配置,也建议多看下nacos官方教程:
https://nacos.io/docs/latest/guide/user/auth/

参考

[1].
hj_服务器操作记录-6月
[2].
https://nacos.io/docs/latest/guide/user/auth/

​ 英特尔发行版 OpenVINO™ 工具套件基于 oneAPI 而开发,可以加快高性能计算机视觉和深度学习视觉应用开发速度工具套件,适用于从边缘到云的各种英特尔平台上,帮助用户更快地将更准确的真实世界结果部署到生产系统中。YOLOv10是清华大学研究人员近期提出的一种实时目标检测方法,通过消除NMS、优化模型架构和引入创新模块等策略,在保持高精度的同时显著降低了计算开销,为实时目标检测领域带来了新的突破。

在本文中,我们将演示如何使用Intel OpenVINO™ C++ API 部署YOLOv10目标检测模型,并使用 OpenVINO™ 异步推理接口实现模型推理加速。下面看一下YOLOv10模型在OpenVINO™上的运行效果吧:

【B站】YOLOv10在CPU上也能轻松实现50+FPS推理—使用OpenVINO C++部署YOLOv10实现异步推理

1. 前言

英特尔发行版 OpenVINO™ 工具套件基于 oneAPI 而开发,可以加快高性能计算机视觉和深度学习视觉应用开发速度工具套件,适用于从边缘到云的各种英特尔平台上,帮助用户更快地将更准确的真实世界结果部署到生产系统中。通过简化的开发工作流程,OpenVINO™ 可赋能开发者在现实世界中部署高性能应用程序和算法。

03b06c0aec22e58235cd01fa6d9b6db4

2024年4月25日,英特尔发布了开源 OpenVINO™ 2024.1 工具包,用于在各种硬件上优化和部署人工智能推理。更新了更多的 Gen AI 覆盖范围和框架集成,以最大限度地减少代码更改。同时提供了更广泛的 LLM 模型支持和更多的模型压缩技术。通过压缩嵌入的额外优化减少了 LLM 编译时间,改进了采用英特尔®高级矩阵扩展 (Intel® AMX) 的第 4 代和第 5 代英特尔®至强®处理器上 LLM 的第 1 令牌性能。通过对英特尔®锐炫™ GPU 的 oneDNN、INT4 和 INT8 支持,实现更好的 LLM 压缩和改进的性能。最后实现了更高的可移植性和性能,可在边缘、云端或本地运行 AI。

YOLOv10是清华大学研究人员近期提出的一种实时目标检测方法,该方法在Ultralytics Python包的基础上进行了多项创新和改进,主要有以下特点

  1. 消除非极大值抑制(NMS)
    :YOLOv10通过引入一致的双重分配策略,在训练时使用一对多的标签分配来提供丰富的监督信号,在推理时使用一对一的匹配,从而消除了对NMS的依赖。这一改进在保持高精度的同时,减少了推理延迟和计算量。
  2. 全面优化的模型架构
    :YOLOv10从推理效率和准确性的角度出发,全面优化了模型的各个组成部分。这包括采用轻量级分类头、空间通道去耦下采样和等级引导块设计等,以减少计算冗余并提高模型性能。
  3. 引入大核卷积和部分自注意模块
    :为了提高性能,YOLOv10在不增加大量计算成本的前提下,引入了大核卷积和部分自注意模块。
  4. 多种模型尺寸可选
    :官方发布了从N到X各种型号的模型,以满足不同应用的需求。这些模型包括超小型版本YOLOv10-N(用于资源极其有限环境)、小型版本YOLOv10-S(兼顾速度和精度)、中型版本YOLOv10-M(通用)、平衡型版本YOLOv10-B(宽度增加,精度更高)、大型版本YOLOv10-L(精度更高,但计算资源增加)以及超大型版本YOLOv10-X(可实现最高的精度和性能)。

通过广泛的实验验证,YOLOv10在多个模型尺度上实现了卓越的精度-延迟权衡。例如,在COCO数据集上,YOLOv10-S在相似精度下比其他实时目标检测方法更快,同时参数和浮点运算量也大幅减少。综上所述,YOLOv10通过消除NMS、优化模型架构和引入创新模块等策略,在保持高精度的同时显著降低了计算开销,为实时目标检测领域带来了新的突破。

image-20240604135821296

2. 项目开发环境

下面简单介绍一下项目的开发环境,开发者可以根据自己的设备情况进行配置:

  • 系统平台:Windows 11
  • Intel Core i7-1165G7
  • 开发平台:Visual Studio 2022
  • OpenVINO™:2024.1.0
  • OpenCV:4.8.0

此处代码开发平台使用的是C++,因此在项目配置时,需要配置第三方依赖库,分别是 OpenVINO™和OpenCV两个个依赖库,其配置方式此处不做详述。

3. 模型获取与INT8量化

为了提升模型的推理速度,我们此处使用 OpenVINO™ 进行推理加速,并使用OpenVINO™NNCF 工具对模型进行一个INT8量化。量化的详细流程可以参考下面notebooks,该notebooks记录了YOLOv10使用OpenVINO™量化的详细流程,链接如下所示:

openvino_notebooks/notebooks/yolov10-optimization

模型量化完成后,我们对比了一下量化前后模型变化,如下图所示:

图片1

4. 定义YOLOv10 Process

4.1 数据预处理

数据预处理此处通过OpenCV实现,将输入的图片数据转为模型需要的数据情况,代码如下所示:

void pre_process(cv::Mat* img, int length, float* factor, std::vector<float>& data) {
    cv::Mat mat;
    int rh = img->rows;
    int rw = img->cols;
    int rc = img->channels();
    cv::cvtColor(*img, mat, cv::COLOR_BGR2RGB);
    int max_image_length = rw > rh ? rw : rh;
    cv::Mat max_image = cv::Mat::zeros(max_image_length, max_image_length, CV_8UC3);
    max_image = max_image * 255;
    cv::Rect roi(0, 0, rw, rh);
    mat.copyTo(cv::Mat(max_image, roi));
    cv::Mat resize_img;
    cv::resize(max_image, resize_img, cv::Size(length, length), 0.0f, 0.0f, cv::INTER_LINEAR);

    *factor = (float)((float)max_image_length / (float)length);
    resize_img.convertTo(resize_img, CV_32FC3, 1 / 255.0);
    rh = resize_img.rows;
    rw = resize_img.cols;
    rc = resize_img.channels();
    for (int i = 0; i < rc; ++i) {
        cv::extractChannel(resize_img, cv::Mat(rh, rw, CV_32FC1, data.data() + i * rh * rw), i);
    }
}

在调用时也相对简单,将相关变量传入即可,代码如下所示:

Mat frame = new frame();
std::vector<float> input_data(640 * 640 * 3);
float factor = 0;
pre_process(&frame, 640, &factor, input_data);

4.2 结果后处理

首先此处定义了一个结果类:

struct DetResult {
    cv::Rect bbox;
    float conf;
    int lable;
    DetResult(cv::Rect bbox,float conf,int lable):bbox(bbox),conf(conf),lable(lable){}
};

然后定义模型的结果处理方式,代码如下所示:

std::vector<DetResult> post_process(float* result, float factor, int outputLength) {
    std::vector<cv::Rect> position_boxes;
    std::vector <int> class_ids;
    std::vector <float> confidences;
    // Preprocessing output results
    for (int i = 0; i < outputLength; i++)
    {
        int s = 6 * i;
        if ((float)result[s + 4] > 0.2)
        {
            float cx = result[s + 0];
            float cy = result[s + 1];
            float dx = result[s + 2];
            float dy = result[s + 3];
            int x = (int)((cx)*factor);
            int y = (int)((cy)*factor);
            int width = (int)((dx - cx) * factor);
            int height = (int)((dy - cy) * factor);
            cv::Rect box(x, y, width, height);

            position_boxes.push_back(box);
            class_ids.push_back((int)result[s + 5]);
            confidences.push_back((float)result[s + 4]);
        }
    }
    std::vector<DetResult> re;
    for (int i = 0; i < position_boxes.size(); i++)
    {
        DetResult det(position_boxes[i], confidences[i], class_ids[i]);
        re.push_back(det);
    }
    return re;

}

最后为了让结果可视化,定义了结果绘制方法,代码如下所示:

void draw_bbox(cv::Mat& img, std::vector<DetResult>& res) {
    for (size_t j = 0; j < res.size(); j++) {
        cv::rectangle(img, res[j].bbox, cv::Scalar(255, 0, 255), 2);
        cv::putText(img, std::to_string(res[j].lable) + "-" + std::to_string(res[j].conf),
            cv::Point(res[j].bbox.x, res[j].bbox.y - 1), cv::FONT_HERSHEY_PLAIN,
            1.2, cv::Scalar(0, 0, 255), 2);
    }
}

上述方式调用依旧十分容易,使用代码如下所示:

std::vector<float> output_data(300 * 6);
std::vector<DetResult> result = post_process(output_data.data(), factor, 300);
draw_bbox(frame, result);

5. 模型推理实现

5.1 基本推理实现

首先实现一下常规的同步推理代码,如下面所示:

void yolov10_infer_without_process() {
    std::string videoPath = "E:\\Text_dataset\\car_test.mov";
    std::string model_path = "E:\\Text_Model\\yolov10s_openvino_model\\yolov10s.xml";
    ov::Core core;
    auto model = core.read_model(model_path);
    auto compiled_model = core.compile_model(model, "CPU");
ov::InferRequest request =compiled_model.create_infer_request();
    cv::VideoCapture capture(videoPath);
    if (!capture.isOpened()) {
        std::cerr << "ERROR: 视频无法打开" << std::endl;
        return;
    }
    float factor = 0;
    request.get_input_tensor().set_shape(std::vector<size_t>{1, 3, 640, 640});
    std::vector<float> inputData(640 * 640 * 3);
    std::chrono::time_point<std::chrono::steady_clock> t_beg;
    std::chrono::time_point<std::chrono::steady_clock> t_end;
    while (true)
    {
        cv::Mat frame;
        if (!capture.read(frame)) {
            break;
        }
        t_beg = std::chrono::high_resolution_clock::now();
        pre_process(&frame, 640, &factor, inputData);
        memcpy(request.get_input_tensor().data<float>(), inputData.data(), 640 * 640 * 3);
        request.infer();
        float* output_data = request.get_output_tensor().data<float>();
        std::vector<DetResult> result = post_process(output_data, factor, 300);
        t_end = std::chrono::high_resolution_clock::now();

        cv::putText(frame, "FPS: " + std::to_string(1000.0 / std::chrono::duration<float, std::milli>(t_end - t_beg).count()) 
            + ", Time: " + std::to_string(std::chrono::duration<float, std::milli>(t_end - t_beg).count()) + "ms",
            cv::Point(20, 40), 1, 2, cv::Scalar(255, 0, 255), 2);
        draw_bbox(frame, result);
        imshow("读取视频", frame);
        cv::waitKey(1);	//延时30
    }
    cv::destroyAllWindows();
    return;
}

5.2 使用异步推理实现

视频一般1s中有25帧左右,这就意味着我们需要1s推理25张图片才可以实现视频推理。一般情况下,在CPU设备推理视觉模型很难实现实时推理,我们在使用 OpenVINO™推理时,经过一些优化技术后,勉强可以实现25FPS的推理,但是如果需要处理其他业务将会很难实现。因此为了提升推理速度,我们采用异步推理技术,实现代码如下所示:

void yolov10_infer_ansy_without_process() {
    std::string videoPath = "E:\\Text_dataset\\car_test.mov";
    std::string model_path = "E:\\Text_Model\\yolov10s_openvino_model\\yolov10s.xml";
    ov::Core core;
    auto model = core.read_model(model_path);
    auto compiled_model = core.compile_model(model, "CPU");
    std::vector<ov::InferRequest>requests = { compiled_model.create_infer_request(), compiled_model.create_infer_request() };
    cv::VideoCapture capture(videoPath);
    // 检查摄像头是否成功打开
    if (!capture.isOpened()) {
        std::cerr << "ERROR: 视频无法打开" << std::endl;
        return;
    }
    float factor = 0;
    requests[0].get_input_tensor().set_shape(std::vector<size_t>{1, 3, 640, 640});
    requests[1].get_input_tensor().set_shape(std::vector<size_t>{1, 3, 640, 640});
    cv::Mat frame;
    capture.read(frame);
    std::vector<float> inputData(640 * 640 * 3);
    pre_process(&frame, 640, &factor, inputData);
    memcpy(requests[0].get_input_tensor().data<float>(), inputData.data(), 640 * 640 * 3);
    requests[0].start_async();
    std::chrono::time_point<std::chrono::steady_clock> t_beg;
    std::chrono::time_point<std::chrono::steady_clock> t_end;
    while (true)
    {
        cv::Mat next_frame;
        if (!capture.read(next_frame)) {
            break;
        }
        t_beg = std::chrono::high_resolution_clock::now();
        pre_process(&next_frame, 640, &factor, inputData);
        memcpy(requests[1].get_input_tensor().data<float>(), inputData.data(), 640 * 640 * 3);
        requests[1].start_async();
        requests[0].wait();
        float* output_data = requests[0].get_output_tensor().data<float>();
        std::vector<DetResult> result = post_process(output_data, factor, 300);
        t_end = std::chrono::high_resolution_clock::now();
        draw_bbox(frame, result);
        cv::putText(frame, "FPS: " + std::to_string(1000.0 / std::chrono::duration<float, std::milli>(t_end - t_beg).count())
            + ", Time: " + std::to_string(std::chrono::duration<float, std::milli>(t_end - t_beg).count()) + "ms",
            cv::Point(20, 40), 1, 2, cv::Scalar(255, 0, 255), 2);
        imshow("读取视频", frame);
        cv::waitKey(1);	//延时30
        frame = next_frame;
        std::swap(requests[0], requests[1]);
    }
    cv::destroyAllWindows();
    return;
}

文章中已经提供了全部代码文件以及模型的获取方式,如果您还有任何疑问,可以下载现成的文件资源。为了方便开发者使用,我们将
代码、测试视频与模型文件以及项目所需依赖
打包,发布到了CSDN上,大家可以根据自己需求进行下载,下载链接:

https://download.csdn.net/download/Grape_yan/89473342

6. 时间测试

最后我们对推理时间进行了测试,分别测试了量化前后模型在同步推理以及异步推理的表现,如下表所示:

Model API PrePocess Inference PostProcess Total FPS
Float 32 Sync 9.72 ms 65.44 ms 0 ms 75.16 ms 13.30
Float 32 Async 15.29 ms 40.52 ms 0 ms 55.81 ms 17.92
INT 8 Sync 15.25 ms 25.64 ms 0 ms 43.89 ms 22.78
INT 8 Async 17.84 ms 1.86 ms 0 ms 19.70 ms 50.76

通过该表可以看出,量化前后,模型推理速度提升了1.5倍,并且通过使用异步推理接口,经过IN8量化后的模型,在本设备上可以实现50FPS的推理速度,与为量化的模型相比,速度推升了2.8倍。通过异步接口,我们可以在CPU下轻松的实现视频推理。

7. 总结

在本文中,我们演示了如何使用Intel OpenVINO™ C++ API 部署YOLOv10目标检测模型,并使用 OpenVINO™ 异步推理接口实现模型推理加速。 最后如果各位开发者在使用中有任何问题,欢迎大家与我联系。

个人账号 - 2

AI Agent框架(LLM Agent):LLM驱动的智能体如何引领行业变革,应用探索与未来展望

1. AI Agent(LLM Agent)介绍

1.1. 术语

  • Agent
    :“代理” 通常是指有意行动的表现。在哲学领域,Agent 可以是人、动物,甚至是具有自主性的概念或实体。
  • AI Agent
    :AI Agent(人工智能代理)是一种能够感知环境、进行决策和执行动作的智能实体。
  • RPA
    :RPA(Robotic Process Automation) 即机器人流程自动化,是一种软件自动化技术。RPA 通过模仿人类在电脑上的手动操作,如打开网站、点击鼠标、键盘输入等,实现业务流程的自动化。RPA 系统可以自动处理大量重复的、基于规则的工作流程任务,例如在银行中,纸质文件输入、文件票据验证、从电子邮件和文件中提取数据、跨系统数据迁移、自动化 IT 应用操作等。RPA 的主要优势包括减少劳动成本、提高生产力、出错率低、可监控的操作和开发周期短。它可以在金融、办公自动化、IT 流程自动化等多个领域发挥重要作用。
  • Copilot
    :即飞机的 “副驾驶”,这里 Copilot 指依托于底层大语言模型(LLM),用户只需说几句话,做出指示,它就可以创建类似人类撰写的文本和其他内容。
  • LangChain
    :LangChain 是一个强大的框架,旨在帮助开发人员使用语言模型构建端到端的应用程序,它提供了一套工具、组件和接口,可简化创建由大型语言模型 (LLM) 和聊天模型提供支持的应用程序的过程。LangChain 是一个语言模型集成框架,其使用案例与语言模型的使用案例大致重叠,包括文档分析和摘要、聊天机器人和代码分析。
  • LLM
    :大型语言模型(LLM)是一种人工智能(AI)算法,它使用深度学习技术和大量大型数据集来理解、总结、生成和预测新内容。
  • 感知记忆(Sensory Memory)
    :感知记忆是信息处理的第一个阶段,它涉及对通过感官接收的信息进行短暂的存储。感知记忆通常只持续几百毫秒到几秒。就像你看到一张美丽的风景照片,感知记忆是大脑对刚刚通过感官接收到的信息的短暂存储。比如,你闭上眼睛后,还能在脑海中短暂地 “看到” 那张照片的颜色和形状,这就是感知记忆在起作用。
  • 短期记忆(Short-term memory)
    :短期记忆就像是你的心智工作台,它能够暂时存储和处理少量信息。比如,当你试图记住一个电话号码时,你可能会重复念叨这个号码,直到你拨打它,这就是短期记忆在工作。所有的上下文学习(In-context Learning)都是利用模型的短期记忆来学习。
  • 长期记忆(Long-term memory)
    :长期记忆就像是一个大仓库,能够存储我们的经验、知识和技能,而且这个存储时间可以非常长,甚至是一生。比如,你学会骑自行车的技能,即使多年不骑,你仍然记得怎么骑,这就是长期记忆。Agent 一般通过外部向量存储和快速检索实现。
  • Memory Stream
    :“记忆” 存储了 Agent 过去的观察、思考和行动序列。正如人脑依靠记忆系统来回溯利用先前的经验制定策略和做出决策一样,Agent 也需要特定的记忆机制来确保其熟练处理一系列连续任务。+ MRKL(Modular Reasoning, Knowledge and Language):MRKL 可以理解为是一种构建 AI 的方式,用于自主代理的神经符号结构,它将推理、知识理解和语言能力作为不同的模块来处理。就像搭积木,每个积木代表 AI 的一个能力,组合在一起就能让 AI 进行复杂的思考和交流。
  • TALM(Tool Augmented Language Models)
    :TOOL 增强的语言模型,是指通过工具或技术来增强的语言处理模型,通常通过微调来实现。例如,一个 AI 聊天机器人,通过接入搜索引擎或其他数据库,能够更准确地回答问题或提供信息。
  • 子目标与分解(Subgoal and decomposition)
    :在解决问题时,Agent 经常会把一个大目标分解成几个小目标(子目标),从而实现对复杂任务的高效处理。比如,准备一顿晚餐,你可能需要先去购物(子目标 1),然后准备食材(子目标 2),最后烹饪(子目标 3)。
  • 反思与完善(Reflection and refinement)
    :Agent 可以对历史的动作进行自我批评和自我反思,从错误中吸取教训,并为未来的步骤进行改进,从而提高最终结果的质量。就像写完一篇文章后,你回顾并修改语法错误或不清晰的表达,使文章更加完善。
  • 思维链(Chain-of-thought, CoT)
    :已成为一种标准的提示技术,用于提高模型在复杂任务中的表现。模型被要求 “一步一步地思考”,将艰巨的任务分解为更小更简单的步骤。思维链将大任务转化为多个可管理的任务,并帮助人们理解模型的思维过程。思维链是解决问题时的逻辑推理过程。比如,你想找出为什么天空是蓝色的,你可能会想:“光是由不同颜色组成的... 蓝色光波短,容易被大气散射... 所以天空看起来是蓝色的。+ 思维树(Tree of Thoughts, ToT):通过在任务的每一步探索多种推理可能性来扩展思维链。它首先将问题分解为多个思考步骤,并在每个步骤中生成多个想法,从而创建一个树状结构。搜索过程可以是 BFS(广度优先搜索)或 DFS(深度优先搜索)。思维村是一种图形化的思维链,它像一棵大树,每个分支代表一个思考的方向或想法,可以帮助我们组织和可视化复杂的思考过程。
  • 自我反思(Self Reflection)
    : 自我反思是指对自己的行为、想法或情感进行深入的思考和分析。就像在一天结束时,回想自己的所作所为,评估自己做得好的地方和需要改进的地方。
  • ReAct
    :将任务中单独的行为和语言空间组合在一起,从而使大模型的推理和行动融为一体。该模式帮助大模型与环境互动(例如使用维基百科搜索 API),并以自然语言留下推理的痕迹。主要包括:Thought:Action\Observation。
  • Reflexion
    :一个让 AI Agent 具备动态记忆和自我反思能力以提高推理能力的框架。沿用了 ReAct 中的设置,并提供简单的二进制奖励。每次行动后,AI Agent 都会计算一个启发式函数,并根据自我反思的结果决定是否重置环境以开始新的试验。这个启发式的函数可以判断是否当下的路径效率低下(耗时过长却没有成功)或包含幻觉(在环境中遇到一连串导致相同观察结果的相同行动),并在出现这两种情况下终止函数。
  • Self-ask
    :Self-ask 可能是指 AI 系统在处理问题时,自主提出问题以引导其思考过程。这类似于人类在面对问题时,会自问:“我接下来应该做什么?” 来推动解决问题的进程。+ 后见链(Chain of Hindsight):通过向模型明确展示一系列过去的输出结果,鼓励模型改进自身的输出结果,使得下一次预测的行动比之前的试验取得更好的成绩。算法蒸馏(Algorithm Distillation)将同样的理念应用于强化学习任务中的跨集轨迹。

1.2. Agent 这个词意义、什么是Agent

1.2.1 Agent由来

有很多人或许会疑惑,Agent 这个东西看起来跟 LLM 也没差得那么远,那为啥最近突然 Agent 那么火,而不称之为 LLM-Application 或者其他的词呢?
这就得从 Agent 的来历上说起了,因为 Agent 是个很古老的术语,甚至可以追溯至亚里士多德和休谟等人的言论。从哲学意义上讲,“代理人”是指具有行动能力的实体,而 “代理” 一词则表示这种能力的行使或体现。而从狭义上讲,“代理”通常是指有意行动的表现;
相应地,“代理人” 一词表示拥有欲望、信念、意图和行动能力的实体。需要注意的是,代理人不仅包括人类个体,还包括物理世界和虚拟世界中的其他实体。重要的是,“代理” 的概念涉及个人的自主性,赋予他们行使意志、做出选择和采取行动的能力,而不是被动地对外部刺激做出反应。

在 20 世纪 80 年代中后期之前,主流人工智能界的研究人员对 Agent 相关概念的关注相对较少,这可能会让人感到惊讶。然而,从那时起,计算机科学和人工智能界对这一话题的兴趣就大大增加了。正如 Wooldridge 等人所言,我们可以这样定义人工智能:“它是计算机科学的一个子领域,旨在设计和构建基于计算机的、表现出智能行为各个方面的 Agent。” 因此,我们可以把 Agent 作为人工智能的核心概念。当 Agent 这一概念被引入人工智能领域时,其含义发生了一些变化。在哲学领域,Agent 可以是人、动物,甚至是具有自主性的概念或实体。然而,在人工智能领域,Agent 是一个计算实体。由于意识和欲望等概念对于计算实体来说似乎具有形而上学的性质,而且我们只能观察机器的行为,包括艾伦 - 图灵在内的许多人工智能研究者建议暂时搁置 Agent 是否 “真正” 在思考或是否真的拥有 “思想” 的问题。相反,研究人员采用其他属性来帮助描述 Agent,如自主性、反应性、主动性和社交能力等属性。也有研究者认为,智能是“看人的眼睛”;它不是与生俱来的、孤立的属性。从本质上讲,AI Agent 并不等同于 Philosophy Agent;相反,它是 Agent 这一哲学概念在人工智能领域的具体化。

现在 AI Agent 也没有完全统一的名称,比如 “AI 代理”、“智能代理”、“智能体” 等等叫法,我们可以通过下面的文章还了解一下什么是 AI Agent,以及下了的技术原理及应用场景等。

1.2.2 什么是 AI Agent

AI Agent(人工智能代理)是一种能够感知环境、进行决策和执行动作的智能实体。 不同于传统的人工智能, AI Agent 具备通过独立思考、调用工具去逐步完成给定目标的能力。比如,告诉 AI Agent 帮忙下单一份外卖,它就可以直接调用 APP 选择外卖,再调用支付程序下单支付,无需人类去指定每一步的操作。 Agent 的概念由 Minsky 在其 1986 年出版的《思维的社会》一书中提出,Minsky 认为社会中的某些个体经过协商之后可求得问题的解,这些个体就是 Agent。他还认为 Agent 应具有社会交互性和智能性。 Agent 的概念由此被引入人工智能和计算机领域,并迅速成为研究热点。但苦于数据和算力限制, 想要实现真正智能的 AI Agents 缺乏必要的现实条件。

大语言模型和 AI Agent 的区别在于 AI Agent 可以独立思考并做出行动,
和 RPA 的区别在于它能够处理未知环境信息
。 ChatGPT 诞生后, AI 从真正意义上具备了和人类进行多轮对话的能力,并且能针对相应问题给出具体回答与建议。 随后各个领域的 “Copilot” 推出,如 Microsoft 365 Copilot、 Microsoft Security Copilot、GitHub Copilot、 Adobe Firefly 等,让 AI 成为了办公、代码、设计等场景的“智能副驾驶”。
AI Agent 和大模型的区别在于:

  • 大模型与人类之间的交互是基于 prompt 实现的,用户 prompt 是否清晰明确会影响大模型回答的效果,例如 ChatGPT 和这些 Copilot 都需要明确任务才能得到有用的回答。
  • AI Agent 的工作仅需给定一个目标,它就能够针对目标独立思考并做出行动,它会根据给定任务详细拆解出每一步的计划步骤,依靠来自外界的反馈和自主思考,自己给自己创建 prompt,来实现目标。如果说 Copilot 是 “副驾驶”,那么 Agent 则可以算得上一个初级的 “主驾驶”。

和传统的 RPA 相比, RPA 只能在给定的情况条件下,根据程序内预设好的流程来进行工作的处理,在出现大量未知信息、难以预测的环境中时, RPA 是无法进行工作的, AI Agent 则可以通过和环境进行交互,感知信息并做出对应的思考和行动。

我们看见的 AI Agent 往往以问答机器人作为交互入口,通过自然语言触发全自动的工作流,中间没有人工介入。由于人只负责发送指令,并不参与对 AI 结果的反馈。

1.2.3 为什么需要 AI Agent

  • LLM 的一些缺点:
    • 会产生幻觉
    • 结果并不总是真实的
    • 对时事的了解有限或一无所知
    • 很难应对复杂的计算
    • 没有行动能力
    • 没有长期记忆能力

比如让 ChatGPT 买一杯咖啡,ChatGPT 给出的反馈一般类似 “无法购买咖啡,它只是一个文字 AI 助手” 之类的回答。但你要告知基于 ChatGPT 的 AI Agent 工具让它买一杯咖啡,它会首先拆解如何才能为你购买一杯咖啡并拟定代用某 APP 下单以及支付等若干步骤,然后按照这些步骤调用 APP 选择外卖,再调用支付程序下单支付,过程无需人类去指定每一步操作。这就是 AI Agent 的用武之地,它可以利用外部工具来克服这些限制。这里的工具是什么呢?工具就是代理用它来完成特定任务的一个插件、一个集成 API、一个代码库等等,例如:

  • Google 搜索:获取最新信息
  • Python REPL:执行代码
  • Wolfram:进行复杂的计算
  • 外部 API:获取特定信息

而 LangChain 则是提供一种通用的框架通过大语言模型的指令来轻松地实现这些工具的调用。我们都知道在执行一个复杂的任务时,我们需要考虑多方面的影响因素,将复杂任务拆分为细小的子任务去执行。AI Agent 的诞生就是为了处理各种复杂任务的,就复杂任务的处理流程而言 AI Agent 主要分为两大类:
行动类、规划执行类
。总而言之,AI Agent 就是结合大模型能去自动思考、规划、效验和执行的一个计算体,以完成特定的任务目标,如果把大模型比作大脑,那 AI Agent 可以理解为小脑 + 手脚。

1.2.4 AI Agent 对比人类与其它 AI 协同的区别

AI Agent 较日前广泛使用的 Copilot 模式更加独立。对比 AI 与人类的交互模式,目前己从过去的嵌入式工具型 AI (例如 siri)向助理型 AI 发展。目前的各类 AI Copilot 不再是机械地完成人类指令,而是可以参与人类工作流,为诸如编写代码、策划活动、优化流程等事项提供建议,与人类协同完成。而AI Agent 的工作仅需给定一个目标,它就能够针对目标独立思考并做出行动,它会根据给定任务详细拆解出每一步的计划步骤,依靠来自外界的反馈和自主思考,自己给自己创建 prompt,来实现目标。如果说 Copilot 是 “副驾驶”,那么 Agent 则可以算得上一个初级的 “主驾驶”。

1.3 AI Agent 案例

1.3.1. AI 虚拟小镇

临近情人节,生活在名为 “Smallville” 小镇上的咖啡店长伊莎贝拉试图举办一场情人节派对,她邀请了自己的闺蜜玛利亚一起布置派对,而玛利亚得知有这么一场派对后,偷偷邀请了暗恋对象克劳斯一同前往…… 在小镇的同一时间线上,年近六旬的汤姆对小镇即将举办的市长选举有着强烈的兴趣,作为一名对政治格外关心的已婚中年男人,他拒绝了伊莎贝拉的情人节派对邀请。以上情节并未发生在现实世界,但也不是人类编造的虚构剧情,它来自一个由 25 名 AI 角色组成的虚拟小镇。而这个小镇上发生的任何事件,都是 AI 之间通过互动随机生成的结果,目前这个小镇已经井井有条地运转了两天。

1.3.2. AutoGPT 做市场调研

假装自己经营一家鞋公司,给 AutoGPT 下达的命令是对防水鞋进行市场调查,然后让其给出 top5 公司,并报告竞争对手的优缺点 :

  • 首先,AutoGPT 直接去谷歌搜索,然后找防水鞋综合评估 top 5 的公司。一旦找到相关链接,AutoGPT 就会为自己提出一些问题,例如「每双鞋的优缺点是什么、每款排名前 5 的防水鞋的优缺点是什么、男士排名前 5 的防水鞋」等。
  • 之后,AutoGPT 继续分析其他各类网站,并结合谷歌搜索,更新查询,直到对结果满意为止。期间,AutoGPT 能够判断哪些评论可能偏向于伪造,因此它必须验证评论者。

执行过程中,AutoGPT 甚至衍生出自己的子智能体来执行分析网站的任务,找出解决问题的方法,所有工作完全靠自己。结果是,AutoGPT 给出了 top 5 防水鞋公司的一份非常详细的报告,报告包含各个公司的优缺点,此外还给出了一个简明扼要的结论。全程只用了 8 分钟,费用为 10 美分。期间也完全没有优化。

AutoGPT 官方公开 Demo 演示:
https://www.bilibili.com/video/BV1HP411B7cG/?vd_source=6a57ee58a99488dc38bda2374baa1c10

2. AI Agent 的框架

上面介绍了 AI Agent 是什么以及一些案例演示,下面的内容将对 AI Agent 背后的技术进行分析。
一个基于大模型的 AI Agent 系统可以拆分为大模型、规划、记忆与工具使用四个组件部分
。6 月,OpenAI 的应用研究主管 Lilian Weng 撰写了一篇博客,认为 AI Agent 可能会成为新时代的开端。她提出了 Agent=LLM + 规划技能 + 记忆 + 工具使用的基础架构,其中 LLM 扮演了 Agent 的 “大脑”,在这个系统中提供推理、规划等能力。

2.1. 大模型 + 规划: Agent 的 “大脑”, 通过思维链能力实现任务分解

LLM 具备逻辑推理能力,Agent 可以将 LLM 的逻辑推理能力激发出来。当模型规模足够大的时候,LLM 本身是具备推理能力的。在简单推理问题上,LLM 已经达到了很好的能力;但在复杂推理问题上,LLM 有时还是会出现错误。事实上,很多时候用户无法通过 LLM 获得理想的回答,原因在于 prompt 不够合适,无法激发 LLM 本身的推理能力,通过追加辅助推理的 prompt,可以大幅提升 LLM 的推理效果。在《Large language models are zero-shot reasoners》这篇论文的测试中,在向 LLM 提问的时候追加 “Let’s think step by step” 后,在数学推理测试集 GSM8K 上的推理准确率从 10.4% 提升到了 40.7%。而 Agent 作为智能体代理,能够根据给定的目标自己创建合适的 prompt,可以更好地激发大模型的推理能力。

通常情况下,一项复杂的任务往往涉及许多步骤。AI Agent 需要首先拆解这些步骤,并提前做好计划。任务的分解的环节可以由三种方式完成:1)在大模型输入简单的提示,比如 “XYZ 的步骤”,或者 “实现 XYZ 的子目标是什么?”;2)使用特定任务的指令,比如在需要写小说的时候要求大模型 “写一个故事大纲”;3)通过人工提供信息。当下普遍的技术模式包括思维链和思维树:

思维链(Chain of Thoughts)

思维链(Chain of Thoughts)已成为一种标准的提示技术,用于提高模型在复杂任务中的表现。模型被要求 “一步一步地思考”,将艰巨的任务分解为更小更简单的步骤。思维链将大任务转化为多个可管理的任务,并帮助人们理解模型的思维过程。

以一个数学题为例,标准 Prompting,模型输入:

问:罗杰有5个网球,他又买了两盒网球,每盒有3个网球。他现在有多少网球?
答:答案是11
问:食堂有23个苹果,如果他们用掉20个后又买了6个。他们现在有多少个苹果?
模型输出:
答:答案是27

可以看到模型无法做出正确的回答。但如果说,我们给模型一些关于解题的思路,就像我们数学考试,都会把解题过程写出来再最终得出答案,不然无法得分。CoT 做的就是这件事,示例如下:CoT Prompting,模型输入:

问:罗杰有5个网球,他又买了两盒网球,每盒有3个网球。他现在有多少网球?
答:罗杰一开始有5个网球,2盒3个网球,一共就是2*3=6个网球,5+6=11。答案是11.
问:食堂有23个苹果,如果他们用掉20个后又买了6个。他们现在有多少个苹果?
模型输出:
答:食堂原来有23个苹果,他们用掉20个,所以还有23-20=3个。他们又买了6个,所以现在有6+3=9。答案是9 

可以看到,类似的算术题,思维链提示会在给出答案之前,还会自动给出推理步骤。思维链提示,
就是把一个多步骤推理问题,分解成很多个中间步骤,分配给更多的计算量,生成更多的 token,再把这些答案拼接在一起进行求解

思维树(Tree of Thoughts)

思维树(Tree of Thoughts)通过在任务的每一步探索多种推理可能性来扩展思维链。它首先将问题分解为多个思考步骤,并在每个步骤中生成多个想法,从而创建一个树状结构。搜索过程可以是 BFS(广度优先搜索)或 DFS(深度优先搜索)。ToT 做 4 件事:
思想分解、思想生成器、状态评估器和搜索算法

ToT Prompt 的例子如下:

假设三位不同的专家来回答这个问题。所有专家都写下他们思考这个问题的第一个步骤,然后与大家分享。然后,所有专家都写下他们思考的下一个步骤并分享。以此类推,直到所有专家写完他们思考的所有步骤。只要大家发现有专家的步骤出错了,就让这位专家离开。请问...

另一方面,试错和纠错在现实世界的任务决策中是不可避免且至关重要的步骤。自我反思帮助 AI Agent 完善过去的行动决策、纠正以前的错误、从而不断改进。当下的技术包括 ReAct、Reflexion、后见链(Chain of Hindsight)等

ReAct(!)

ReAct:将任务中单独的行为和语言空间组合在一起,从而使大模型的推理和行动融为一体。该模式帮助大模型与环境互动(例如使用维基百科搜索 API),并以自然语言留下推理的痕迹。

React 论文《ReAct: Synergizing Reasoning and Acting in Language Models》:
https://react-lm.github.io/

问题:除了Apple遥控器,还有什么其他设备可以控制相关软件?相关软件指的是Apple遥控器最早可以控制的软件。
说明:
1.Apple遥控器最早只能控制Front Row软件。
2.Front Row软件可以被两种设备控制,Apple遥控器和键盘的功能键。
3.所以,正确答案是键盘的功能键。

传统方式1,直接问LLM。得到结果:
iPod。
答案错误,显然是在胡言乱语。因为LLM的内容生成,是根据之前所有的字预测后续内容,所以,我推测,出现这个结果很可能因为,LLM其实并不知道相关答案,只是Apple遥控器和iPod的语义很接近,所以才得出iPod这个结论。

传统方法2,思维链。得到结果:
想法:让我们一步一步思考这个问题。Apple遥控器可以控制Apple电视。Apple电视可以被iPhone、iPad和iPod Touch控制。
答案:iPhone、iPad和iPod Touch控制。
答案错误,思维链范式,其实也是在胡编,因为它不知道答案。

传统方法3,动作范式。得到结果:
动作1:搜索【Apple 遥控器】
结果1:Apple遥控器是一种遥控器……最早可以控制“Front Row”……

动作2:搜索【Front Row】
结果2:无结果

动作3:搜索【Front Row软件】
结果3:FrontRow是一种过时软件……

动作4:完成【是的】
答案错误,动作范式,即隐式推理,也没有得到正确答案。不难理解,隐式推理对LLM的推理能力要求很高,因为他会隐去思考过程。我觉得当LLM推理能力达到一定程度时,应该也能得出正确答案。

ReAct方法,即推理+动作。得到结果:
想法1:我需要搜索Apple遥控器,并找到Apple遥控器最早可以控制的软件。
动作1:搜索【Apple 遥控器】
结果1:Apple遥控器是一种遥控器……最早可以控制“Front Row”……

想法2:Apple遥控器最早可以控制Front Row。我需要搜索Front Row下一步,找出还有什么其他设备可以控制他
动作2:搜索【Front Row】
结果2:无结果,可以试试“Front Row Seat to Earth”和“Front Row软件”

想法3:Front Row没有找到,我可以搜索“Front Row软件”
动作3:搜索【Front Row软件】
结果3:FrontRow是一种过时软件……可以被Apple遥控器和键盘功能键控制。

想法4:现在我知道答案了。
动作4:完成【键盘功能键】
答案正确。通过显式推理,加上动作,LLM Agent自主找到了答案。整个过程感觉就像是个不是特别聪明的孩子,要把想法写出来,然后去综合所有想法和观察,接着再做出相应的动作。但显然这个方法很有效,它最终找到了答案。


Reflexion

Reflexion:一个让 AI Agent 具备动态记忆和自我反思能力以提高推理能力的框架。沿用了 ReAct 中的设置,并提供简单的二进制奖励。每次行动后,AI Agent 都会计算一个启发式函数,并根据自我反思的结果决定是否重置环境以开始新的试验。这个启发式的函数可以判断是否当下的路径效率低下(耗时过长却没有成功)或包含幻觉(在环境中遇到一连串导致相同观察结果的相同行动),并在出现这两种情况下终止函数。

2.2. 记忆:用有限的上下文长度实现更多的记忆

记忆模块负责存储信息,包括过去的交互、学习到的知识,甚至是临时的任务信息
。对于一个智能体来说,有效的记忆机制能够保障它在面对新的或复杂的情况时,调用以往的经验和知识。例如,一个具备记忆功能的聊天机器人可以记住用户的偏好或先前的对话内容,从而提供更个性化和连贯的交流体验。

对 AI 智能体系统的输入会成为系统的记忆,与人类的记忆模式可实现一一映射
。记忆可以定义为用于获取、存储、保留以及随后检索信息的过程。人脑中有多种记忆类型,如感觉记忆、短期记忆和长期记忆。而对于 AI Agent 系统而言,用户在与其交互过程中产生的内容都可以认为是 Agent 的记忆,和人类记忆的模式能够产生对应关系。感觉记忆就是作为学习嵌入表示的原始输入,包括文本、图像或其他模态;短期记忆就是上下文,受到有限的上下文窗口长度的限制;长期记忆则可以认为是 Agent 在工作时需要查询的外部向量数据库,可通过快速检索进行访问。目前 Agent 主要是利用外部的长期记忆,来完成很多的复杂任务,比如阅读 PDF、联网搜索实时新闻等。任务与结果会储存在记忆模块中,当信息被调用时,储存在记忆中的信息会回到与用户的对话中,由此创造出更加紧密的上下文环境。

为了解决有限记忆时间的限制,通常会用到外部存储器。常见的做法是将信息的嵌入表示保存到可支持快速的最大内积搜索(MIPS)的向量存储数据库中。向量数据库通过将数据转化为向量存储,解决大模型海量知识的存储、检索、匹配问题
。向量是 AI 理解世界的通用数据形式,大模型需要大量的数据进行训练,以获取丰富的语义和上下文信息,导致了数据量的指数级增长。向量数据库利用人工智能中的 Embedding 方法,将图像、音视频等非结构化数据抽象、转换为多维向量,由此可以结构化地在向量数据库中进行管理,从而实现快速、高效的数据存储和检索过程,赋予了 Agent“长期记忆”。同时,将高维空间中的多模态数据映射到低维空间的向量,也能大幅降低存储和计算的成本,向量数据库的存储成本比存到神经网络的成本要低 2 到 4 个数量级。

Embedding 技术和向量相似度计算是向量数据库的核心
。Embedding 技术是一种将图像、音视频等非结构化数据转化为计算机能够识别的语言的方法,例如常见的地图就是对于现实地理的 Embedding,现实的地理地形的信息其实远远超过三维,但是地图通过颜色和等高线等来最大化表现现实的地理信息。在通过 Embedding 技术将非结构化数据例如文本数据转化为向量后,就可以通过数学方法来计算两个向量之间的相似度,即可实现对文本的比较。向量数据库强大的检索功能就是基于向量相似度计算而达成的,通过相似性检索特性,针对相似的问题找出近似匹配的结果,是一种模糊匹配的检索,没有标准的准确答案,进而更高效地支撑更广泛的应用场景。

2.3. 工具:懂得使用工具才会更像人类

AI Agent 与大模型的一大区别在于能够使用外部工具拓展模型能力
。懂得使用工具是人类最显著和最独特的地方,同样地,也可以为大模型配备外部工具来让模型完成原本无法完成的工作。ChatGPT 的一大缺点在于,其训练数据只截止到了 2021 年底,对于更新一些的知识内容它无法直接做出回答。虽然后续 OpenAI 为 ChatGPT 更新了插件功能,能够调用浏览器插件来访问最新的信息,但是需要用户来针对问题指定是否需要使用插件,无法做到完全自然的回答。AI Agent 则具备了自主调用工具的能力,在获取到每一步子任务的工作后,Agent 都会判断是否需要通过调用外部工具来完成该子任务,并在完成后获取该外部工具返回的信息提供给 LLM,进行下一步子任务的工作。OpenAI 也在 6 月为 GPT-4 和 GPT-3.5 更新了函数调用的功能,开发者现在可以向这两个大模型描述函数,并让模型智能地选择输出包含调用这些函数的参数的 JSON 对象。这是一种更可靠地将 GPT 的功能与外部工具和 API 相连的新方法,允许开发者更可靠地从模型中获得结构化的数据,为 AI 开发者提供了方便。实现调用工具的方法就是编写大量的工具调用数据集来进行模型的微调。

总结一下
AI Agent 的原理主要包括感知、分析、决策和执行四大能力
。这些能力相互协同,构成了 AI Agent 的基本工作原理。首先是感知能力,通过传感器获取外部环境的信息,使 AI Agent 能够对周围的情况有所了解。其次是分析能力,通过对感知到的信息进行分析和处理,提取有用的特征和模式。然后是决策能力,AI Agent 基于分析结果进行决策,制定相应的行动计划。最后是执行能力,将决策转化为具体的行动,实现任务的完成。这四大能力相互配合,使得 AI Agent 能够在复杂的环境中高效地运行和执行任务。

3. AI Agent 的应用进展

3.1. AutoGPT:推动 AI Agent 研究热潮

AutoGPT 将 AI Agent 概念带 “出圈” 。 2023 年 3 月, 开发人员 Significant Ggravitas 在 GitHub 上发布了开源项目 AutoGPT,它以 GPT-4 为驱动基础, 允许 AI 自主行动,完全无需用户提示每个操作。给 AutoGPT 提出目标,它就能够自主去分解任务、执行操作、完成任务。 AutoGPT 仍存在成本高、响应慢、出现死循环 bug 等缺点。 Auto-GPT 采用的是 GPT-3.5 和 GPT-4 的 API, 而 GPT-4 的单个 token 价格为 GPT-3.5 的 15 倍。 假设每次任务需要 20 个 step(理想状况下),每个 step 会花费 4K tokens 的 GPT-4 使用量, prompt 和回复的平均每一千 tokens 花费是 0.05 美元(因为实际使用中回复使用的 token 远远多于 prompt),假设汇率为 1 美元 = 7 人民币,那么花费就是 20
4
0.05*7=28 元人民币。而这仅是理想状况下,正常使用中经常出现需要拆分出几十上百个 step 的任务,这时单个任务的处理成本就会难以接受。而且 GPT-4 的响应速度远远慢于 GPT-3.5,导致 step 一多的时候任务处理会变得很慢。并且 AutoGPT 在遇到 GPT-4 无法解决的 step 问题时,就会陷入死循环中,不断重复没有意义的 prompt 和输出,造成大量的资源浪费和损失。

3.2. 游戏领域应用:西部世界小镇

斯坦福西部世界小镇首次创造了多个智能体生活的虚拟环境。 2023 年 4 月, 斯坦福大学的研究者们发表了名为《 Generative Agents: Interactive Simulacra of Human Behavior》 的论文,展示了一个由生成代理( Generative Agents)组成的虚拟西部小镇。 这是一个交互式的沙盒环境,在小镇上,生活着 25 个可以模拟人类行为的生成式 AI Agent。它们会在公园里散步,在咖啡馆喝咖啡,和同事分享当天的新闻。 甚至一个智能体想举办情人节排队,这些智能体在接下来的两天里,会自动传播派对邀请的消息,结识新朋友,互相约对方一起去派对,还会彼此协调时间,在正确的时间一起出现在派对上。 这种 Agent 具有类似人的特质、独立决策和长期记忆等功能,它们更接近于 “原生 AI Agent”。在这种合作模式下, Agent 不仅仅是为人类服务的工具,它们也能够在数字世界中与其他 Agent 建立社交关系。

西部世界小镇中 Agents 的架构 记忆流包含大量的观察、检索过程记忆流是西部世界小镇中 AI Agents 的架构核心。 小镇中的 Agents 包含三大重要的基本要素:记忆、反思和规划,相比前面提到的几个核心组件略有调整。这三大基本要素都基于一个核心:记忆流( Memory Stream),记忆流存储了 Agent 的所有经历记录,是一个包含了多个观察的列表,每个观察都包含了事件描述、创建时间以及最近一次访问的时间戳, 观察可以是 Agent 自己的行为或从其他人那里感知到的行为。为了检索最重要的记忆以传递给语言模型,研究者确定了检索过程中需要考虑的三个因素:最近性、重要性和相关性。通过确定每条记忆基于这三个因素的分数,最后加总起来得到权重最高的记忆,作为 prompt 的一部分传递给大模型,以此来决定 Agent 的下一步动作。反思和规划都是基于记忆流中的观察来进行更新与创建的。

3.3. HyperWrite:推出首个个人 AI 助理 Agent

HyperWrite 推出首个个人 AI 助理 Agent。 2023 年 8 月 3 日, 人工智能初创公司 HyperWrite 正式推出了 AI Agent 的应用 Personal Assistant,希望可以成为人类的 “数字助手”。 作为 HyperWrite 的投资者,生成式 AI 初创企业 Cohere 联合创始人 Aidan Gomez 表示:“我们将开始第一次看到真正的个人 AI 助理” 。作为个人助理 Agent,它可以帮助用户整理邮箱并起草回复、帮助用户订机票、订外卖、整理领英上适合的简历等,将 AI 能力无缝接入到用户的日常生活和工作流中。目前该工具还处于试用阶段,主要适用于网页浏览器场景。

Personal Assistant 可以自主在浏览器中完成指定任务。 Personal Assistant 现在是以浏览器拓展插件的形式来提供服务的,用户在安装完插件并注册账户后即可开始试用。其初始页面类似于 New Bing 这样的搜索引擎,仅提供一个自然语言交互的聊天框。用户输入其想要完成的目标后,该插件就会新建一个浏览器页面,并在页面以侧边栏形式展示其进行的每一步操作与思路。以 “给我一些美国现在关于 AI Agent 的新观点” 这一目标为例,该个人助理会先去进行相关的搜索,然后打开相关的文章页面进行阅读并总结观点,在完成阅读和总结后,它会将结果汇总并返回到聊天框中,整体用时约为 2 分钟。目前个人 AI 助理能力仍旧有限,但潜力可期。

目前 HyperWrite Personal Assistant 仅为 0.01 版本,其功能仍相对有限,也存在一些出错的问题,并且响应过程也较为缓慢。但我们认为, AI Agent 自此迈出了走向个人消费者领域的第一步,随着未来大模型能力的进一步提升,以及算力基础设施的不断普惠,个人 AI 助理的发展潜力值得期待。

3.4. ModelScopeGPT:国内大模型调用工具

ModelScopeGPT 是阿里云 MaaS 范式在模型使用层的重要映射,旨在建立大模型生态。 阿里云表示,构建 ModelScopeGPT 的数据集和训练方案将会对外开放,供开发者自行调用,开发者可以根据需要对不同的大模型和小模型进行组合, 帮助开发者多、快、好、省地使用大模型。 目前在 AI 开发者圈,魔搭社区已成中国大模型第一门户。所有模型生产者都可以上传自己的模型,验证模型的技术能力和商业化模式,并与其他社区模型进行协作,共同探索模型应用场景。ModelScopeGPT 则实现了将模型生产力进行自由组合,继续强化阿里云在大模型生态建设中的领先地位。

3.5. Inflection AI: 高情商个人 AI --- Pi

Inflection AI 推出主打情感陪伴的个人 AI——Pi。 Inflection AI 是一家成立于 2022 年的人工智能初创公司,目前公司的估值已经突破 40 亿美元,在人工智能领域仅次于 OpenAI。在 2023 年 5 月,公司推出了旗下的个人 AI 产品 Pi。 与 ChatGPT 不同, Pi 从未以专业性与替代人工作为宣传。它不能写代码,也不能帮我们生产原创内容,与时下流行的通用聊天机器人相反, Pi 只能进行友好的对话,提供简洁的建议,甚至只是倾听。它的主要特征是富有同情心、 谦虚好奇、幽默创新,具有良好的情商,可以根据用户的独特兴趣和需求提供无限的知识与陪伴。 Inflection 自开发 Pi 开始,就确定了 Pi 将作为个人智能( Personal Intelligence) ,而不仅仅是辅助人工作的工具。

Pi 的核心是公司研发的 Inflection-1 大模型,性能媲美 GPT-3.5。 Inflection-1 是 Inflection AI 推出的大模型,根据公司的评估测试, Inflection-1 在多任务语言理解、常识问题等多项测试中的性能都略胜于 GPT-3.5、 LLaMA 等常用的大模型,但在代码能力上要落后于 GPT-3.5。不过这是公司的差异化竞争所在, Pi 作为一个以情感陪伴为主的 Agent 并不需要拥有很强的代码和辅助工作能力。

和辅助工作的 Agent 不同, Pi 能够满足更多的情感陪伴需求。作为一个具有高情商的 AI Agent,Pi 能够以更加日常和生活化的语言和用户进行交流,而不是以一个冰冷的工作 AI 的口吻。 Pi 的回复非常贴近生活, 语气十分得体,而它对你当下状态和事态发展的关心就像心理医生或者你最好的朋友。当 Pi 在回复可能带有负面情绪的问题时,它也会避免使用任何俏皮的表情或者轻快的口吻去冒犯用户。 它甚至会在回复中使用 emoji,让用户觉得更像是和真正的人类在进行对话一样。Pi 还能够记住与用户的对话内容,并随着时间的推移而更加了解用户。 Pi 的出现,弥补了传统型人工智能对人类情绪欲望的忽视。我们认为,类似于 Pi 这样能够提供情绪价值的个人 AI Agent 存在着较大的市场空间。

3.6. AgentBench: LLM 的 Agent 能力评估标准

AgentBench 评价 LLM 作为 Agent 的能力 常用的 LLM 的 Agent 能力排名 清华大学联合团队提出世界首个大模型 AI Agent 能力的评估标准。 尽管当前 AI 智能体研究异常火热,但 AI 行业缺乏一个系统化和标准化的基准来评估 LLM 作为 Agent 的智能水平。 2023 年 8 月, 清华大学、俄亥俄州立大学、加州大学伯克利分校的研究团队便提出了首个系统性的基准测试——AgentBench,用来评估 LLM 作为 Agent 在各种真实世界挑战和 8 个不同环境中的能力表现(如推理和决策能力)。 这 8 个环境分别是:操作系统、数据库、知识图谱、卡牌对战游戏、家务事、横向思维谜题、 网络购物、网页浏览。基于这 8 个环境,研究团队设计了不同的真实世界挑战,涵盖了代码场景和生活场景,比如用 SQL 语言从一些表格里提取需要的数、 玩卡牌游戏取得胜利、从网页预订机票等。

GPT-4 性能遥遥领先,开源模型能力显著弱于闭源模型。 研究者选择了 25 种主流的大模型 API 来进行 Agent 能力评估,涵盖了闭源模型(如 OpenAI 的 GPT-4、 GPT-3.5 等)和开源模型( LLaMA 2 和 Baichuan 等)。 根据测试结果来看, GPT-4 基本上在所有环境中都占据领先地位,是名副其实的当前大模型能力边界。 闭源模型 Anthropic 的 Claude 以及 OpenAI 的 GPT-3.5 水平相差不大,而常见的一些开源模型 Vicuna、 Dolly 等由于尺寸和闭源模型相差了至少一个数量级,性能评估显著较弱。 我们认为,虽然 LLM 能够在自然语言交流等 NLP 上达到基本的类人水平,但在关注行动有效性、上下文长度记忆、多轮对话一致性和代码生成执行等 Agent 重要能力上的表现仍旧相对落后,基于 LLM 的 AI Agent 的发展空间仍具潜力。

3.7 AI Agent 在安全业务里应用场景

目前在网络安全垂直领域中,还是以 Microsoft Security Copilot 为主要产品形态的形势出现,还是以 “副驾” 来作为定位,还没有出现安全业务中的 AI Agent,但相信不久一定会有病毒分析的 AI Agent、红军 AI Agent、蓝军 AI Agent 等等。

4. AI Agent 未来发展趋势判断

结合目前学术界和产业界基于 LLM 开发的 AI Agent 应用情况,我们将目前 AI Agent 划分为两大类:

  • 自主智能体,力图实现复杂流程自动化。当给定自主智能体一个目标时,它们能自行创建任务、完任务、创建新任务、重新确定任务列表的优先级、完成新的首要任务,并不断重复这个过程,直到完成目标。准确度要求高,因而更需要外部工具辅助减少大模型不确定性的负面影响。
  • 智能体模拟,力图更加拟人可信。 分为强调情感情商的智能体以及强调交互的智能体,后者往往是在多智能体环境中,可能涌现出超越设计者规划的场景和能力,大模型生成的不确定性反而成为优势,多样性使其有望成为 AIGC 重要组成部分。

《AIGC 应用层十大趋势》报告中调研表明,所有企业都认为 AI Agent 是 AIGC 发展的确定性方向,50% 的企业已经在某项工作中进行了 AI Agent 的试点,另有 34% 的企业正在制定 AI Agent 的应用计划。这个报告,也对 AI Agent 发展趋势做了两点预测:AI Agent 让 “人机协同” 成为新常态,个人与企业步入 AI 助理时代。AI Agent 能够帮助未来企业构建以 “人机协同” 为核心的智能化运营新常态。AI Agent 变革未来生产力的组织形式,对抗组织熵增。未来企业工作任务将在 AIGC 的助推作用下变得日益原子化和碎片化,复杂的流程将被无限拆解,再进行灵活的编排和组合,每个环节的效能和潜力都将被 AI 持续挖掘。从供给端看,“人 + AI 数字员工”的高效协同模式将为大型企业对抗组织熵增提供理想的解法。

4.1. 自主智能体:自动化,新一轮生产力革命

自主智能体,力图实现复杂流程自动化。真格基金管理合伙人戴雨森将 AI 和人类协作的程度类比为自动驾驶的不同阶段, AI Agent 约为自动驾驶的 L4 阶段, Agent 完成任务,人进行外部辅助和监督。自主智能体有望带来软件行业交互方式和商业模式变革:

  • 交互方式变革
    : 相比过去的 APP / 软件,从人适应应用变成应用适应人, Agent 的决策 / 规划 / 执行等环节需要更深的用户需求理解以及更强的工程细节打磨。如目前 Agent 运行中常常遇见无休止的扩展、误解输出格式等问题,这类问题不单单依靠大模型能力提升,对 Agent 架构的设计和垂类数据的学习也有要求。
  • 商业模式变革
    : 按服务内容收费转换成按 token 收费,对 Agent 功能实用性要求更高。基座大模型能力固然重要,但其只能解决下限问题,在实际企业应用场景中自主智能体的架构设计、工程能力、垂类数据质量等也至关重要,垂类 / 中间件玩家亦有机会。 准确度和效率是自主智能体重要指标(决策式 AI 更擅长做的事,也意味着更低的容错度),企业对于低门槛定制 AGENT 也存在需求,专注具体领域 / 提供 AGENT 框架的玩家仍有可为。

典型代表:

  • AutoGPT
  • 代码开发类 GPT Engineer
  • 科研类 GPT Researcher
  • 创作类 ShortGPT+ 多智能体: AGENT 团队完成复杂开发任务,如 MetaGPT、AutoGEN

4.2. 智能体模拟:拟人化,新的精神消费品

陪伴类智能体强调情感情商等人类特征,具有 “人格”,且能够记住与用户的历史交流。

  1. LLM 在自然语言理解能力上的重要突破使陪伴类智能体在技术上成为可能,
  2. GPT4 在情商上显著高于以往其他大模型,随着大模型情商迭代、多模态技术发展,有望出现更加立体拟人可信、能够提供较高情绪价值的陪伴智能体。

研究机构认为国内情绪消费市场仍有较大想象空间(社会婚姻观念转变、现代工作生活节奏紧张下,国民孤独感增加),陪伴类智能体或受益于情绪消费趋势红利,成为 LLM 时代重要的 AI 原生应用。从用户陪伴性需求的第一性原理出发, 我们预计陪伴类智能体大部分商业价值集中在 IP 上, 基于此我们更加看好当前具备丰富 IP 储备或者能让用户定制智能体的玩家:

  • 参考人的陪伴: 陌生人社交和秀场直播属于线上陪伴代表性应用,前者核心问题在于用户间建立一定情感联系后会转到最常用的社交平台,后者用户价值会逐渐集中到头部主播,而非平台。
  • 参考物的陪伴: 潮玩等消费品有一定的陪伴属性,受众大部分支出花在自己喜欢的 IP。典型代表:
    • 陪伴类:高情商、具备人格特征,如 Pi
    • 平台化娱乐化,如 Character.AI、 Glow 等
    • 游戏世界 AI 玩家,如 Voyager+ Smallville 小镇,类西部世界的模拟社会+ 网易《逆水寒》 手游, AI NPC 提高玩家体验
    • 昆仑万维《Club Koala》 虚拟世界更可信

4. 总结

至此,本文的探讨已圆满落幕,我们深入剖析了AI Agent这一前沿科技领域的全貌。从基础概念的澄清,到技术原理的细致解构,再跨越至丰富多样的应用场景探索,直至展望其激动人心的未来发展趋势,每一步都凝聚着对AI Agent技术潜力的无限憧憬。诚然,文中提及的诸多项目尚处于学术研究与实践探索的初期阶段,以论文形式呈现的实验性成果,恰似璀璨星辰,预示着大模型技术未来航向的无限可能。这些星星之火,或将点燃AI应用的燎原之势,催生出前所未见的新领域、新机遇,为社会的每一个角落注入智能的活力。

展望未来,AI Agent技术有望在未来五年内实现跨越式发展,其普及程度将远超预期,深刻渗透并重塑千行百业的运作模式。我们期待,这一技术不仅能够在效率上实现质的飞跃,为传统行业带来颠覆性的改变,更能作为创新驱动的核心力量,开启一个智能互联、高效协同的新时代。让我们共同见证,AI Agent技术如何携手各行各业,绘制出一幅幅生动绚丽的智能未来图景。

更多优质内容请关注公号:汀丶人工智能;会提供一些相关的资源和优质文章,免费获取阅读。

参考链接:

1.细说复旦大学,斯坦福大学智能代理 AI-Agent(二更)
https://www.zhihu.com/tardis/zm/art/657737603?source_id=1003

2.AI Agent,启动!复旦 NLP 团队发 86 页长文综述,智能体社会近在眼前
https://cloud.tencent.com/developer/article/2351355
https://zhuanlan.zhihu.com/p/656676717
3.AI Agent 引爆 AGI 时代,十篇研报透视 AI 智能体的现在与未来
https://mp.weixin.qq.com/s/oc08ID6FUb85_wfNeRrDtw
4.一篇文章掌握智能体(Agent ) 产品的分析框架
https://mp.weixin.qq.com/s/k0utZChaXQQJza3YCaxJSw
5.《综述:全新大语言模型驱动的 Agent》——4.5 万字详细解读复旦 NLP 和米哈游最新 Agent Survey
https://zhuanlan.zhihu.com/p/656676717
6.斯坦福的「虚拟小镇」开源了:25 个 AI 智能体照进《西部世界》
https://cloud.tencent.com/developer/article/2325908
7.OpenAI 现场演示官方版 AutoGPT!创作画画发推一条龙,自主调用外部应用完成任务
https://wallstreetcn.com/articles/3687251
8.数字员工、超级个体、具身智能,AI Agent 未来发展十大研究方向
https://baike.baidu.com/tashuo/browse/content?id=7e0fcb27cb24c9f4478d8de5
9.九大最热门的开源 AI Agent 框架
https://www.chatdzq.com/news/1753957671170928641

C
语言是编译型语言,先编译再运行,通常用
gcc进行编译,于是安装了Ubuntu操作系统。至于编辑器,
VS Code
也能用,先
sudo apt install build-essential gdb,再在VS Code安装
C/C++ extension,就可以进行开发了。

C语言程序都是
.c文件结尾,

新建一个hello_world
.c 文件

#include <stdio.h>
int main(void) {
printf(
"Hello World \n");return 0;
}

#include<stdio.h>,#表示指令,include 包含,stdio.h 是一个以.h结尾的文件,连起来就是把stdio.h文件包含进来,把stdio.h文件内容复制粘贴到 #include <stdio.h>这一行代码所在的地方。int main(void) 定义main函数,函数中调用printf, 然后返回0。C程序都是从main函数开始执行,所以必须有main函数。main函数调用哪个C函数,就要include这个函数所在的头文件。所以一个C语言程序要包含main函数,调用系统函数,还要include头文件。
打开VS Code命令行,gcc hello_world.c进行编译,生成了
a.out
,再
./a.out,输出Hello World。

变量声明

使用变量之前,要先声明变量,就是先在内存中开辟一块空间,再使用。C语言是强类型语言,声明变量的时候要指定变量类型。基本数据类型有整数,字符,小数

整数有int, short,long类型,主要是所占的内存空间不同,能存储的数值范围不同。C语言的整数还分有符号(signed)和无符号(unsigned)。有符号就是它可以存储正数,负数和0。无符号则只能存储0和正数。默认情况下,所有整数类型都是singed的。如果要用无符号整型,用unsiged int, unsiged short, unsigned long,在类型前面加unsigned。
浮点数(小数)就是float,double,long double,浮点数全是有符号的。C语言没有规定每一个类型的所占内存空间是多大,也就是说,在不同的机器上,int可能占2个字节,也可能占4个字节,它只保证了大的类型要大于等于小的类型所占的空间,为此,提供了sizeof操作符, 获取每一个类型所占的字节。后面出现了int8_t,int16_t, int32_t, int64_t类型, 在每一个机器上都占8,16,32,64个字节。

char类型,用来存储字符(字母或标点符号),比如 'A',所以它只占1个字节。但实际上,char类型存储的是数字,存储字符的时候,根据
字符编码表,把字符转化成数字,
因此char类型也可以用来表示小的整数,不过,有些编译器把char实现为有符号类型,而有些编译器把它实现为无符号的类型,所以如果要表示数字,最好使用singed char 或unsigned char。转义序列(字符),就是代码中不能或者很难用键盘输入的字符,比如,换行符,如果按enter, 编辑器会解析成下一行,不会在代码中保留下来,还有空格。 The space, tab and the newline are often referred to collectively as white space character, because they cause space to appear rather than making marks on the page when they are printed.

C语言没有布尔类型,使用数字来表示布尔类型,0表示false,1表示true。但后来, 增加了_Bool 类型和<stdbool.h>,可以使用bool声明变量,也可以使用true 和false, 但本质上还是0和1,它们只是对0 和1 进行重定义。

变量名的长度:编译器只识别变量名的前63个字符,后面的字符会忽略,所以变量名最好不要超过63个字符。声明了变量,就要给它赋值,声明变量的同时进行赋值称为初始化。需要注意的是,字面量也是有类型。3就是int类型,整数默认是int类型,如果字面量特别大,int放不下,那该数就成了long。3.0是double类型,浮点数字面量默认是double类型。后面加f表示float类型,3.0f是float类型。字面量有类型,赋值时不要类型不匹配,因为赋值时,类型不匹配也不会报错。int a = 33.33;  会把double类型的33.33 转化成给int,a的值就是33,C竟然不报错。

#include <stdio.h>#include<stdbool.h>

int main(void) {int num = 5;char grade = 'a'; //char类型用单引号
    signed char char_num= 3;float f = 4.0f;double d = 3.0;bool isTrue = false;//当使用printf输出int时,用%d.使用%c 来打印字符。 浮点型用%f进行输出。
    printf("num: %d, grade: %c, char_num: %d, float: %f, double: %f, bool_num: %d \n",
num, grade, char_num, f, d, isTrue);
//可以使用sizeof() 查看类型占多少个字节,比如sizeof(int). 在printf 中使用%zd进行输出 printf("size of int: %zd \n", sizeof(num));
}

操作符、表达式和语句

算术运算符(+, -, *, /, %, ++, --) 、比较操作符(>, <, ==, >=, <=) 、逻辑运算符(, &&, || , !) ,所有语言都差不多。操作符加上它的操作数就是表达式,3+4就是表达式,表达式后面加;号就成了语句,C语句以分号结尾,3+4;就是语句,单个分号;也是一个语句,表示空语句,什么都不做。语句就是对计算机下达的指令,C程序由一条条语句组成。条件语句(if else),循环语句(while, for) 也是和其他语言一样,几个细节需要注意

两个整数相除,结果为整数,舍去小数(truncate toward zero)。取模运算,不能用于小数。a÷b=c…d,c是商,d是余数. c的数值是|a|/|b|, 正负号由a和b的负号数量决定;d的正负号和a相同,数值为 a-(a/b)*b.

-11 %5的运算

浮点数的计算是不精确的,因为浮点数并不能准确地表示小数,为什么呢?先看一下,在0和1之间有多少个整数?只有2个,0和1。但有多少个小数呢?0.1, 0.01, 0.001, 0.0.......01, 无穷多个,由于计算机内存有限,所以无法表示所有的小数,只能表示某些特定小数,其它无法表示的小数用近似数表示,这就会产生误差。小数点位数越多,越能表示的精确, 如果一个小数是0.123456798345, float 就会 取前7-8位,double 类型会取前15-16 位小数(四舍五入),会更精确一点。由于计算机是二进制,它也不能很好的表示十进制小数,比如,0.1, 如果把0.1 赋值给一个浮点数变量,变量实际存储的值可能是 0.0999999999999987,同样存在四舍五入的原因,所以浮点数比较大小,可以使用> 或 <,最好不要比较相等,因为稍微四舍五入,就会导致两数不相等,尽管逻辑上,它们应该相等。可以使用近似比较,只要两数相减,大于我们认为的最小值,就认为它们相等。(fabs(response - ANSWER) > 0.0001。

自动类型转换。计算机在做算术运算时,操作数必须大小相同且存储方式一致。大小指的是操作数在内存中占用的空间。比如int是4个字节,char是1个字节,它们的大小就不同。计算机会把两个16-bit的整数直接相加,但它不会把一个16-bit的整数和一个32-bit的整数直接相加,也不会把32-bit的整数和32-bit的小数直接相加。如果表达式中,操作数类型不一致,就会自动类型提升(小类型转换成大类型),char 和short, 不管是有符号,还是无符号,都会提升成int 或unsigned int。3+4.5,3会提升double类型,整个表达式的结果是double类型的7.5。表达式也有类型,赋值的时候要注意,因为赋值时类型不匹配,也不会报错。int a = 3 + 4.5;  7.5 会自动转化成int,a的值为7。如果必须这样操作,最好使用强制类型转化,把想要转化成的类型用()括起来,然后放到要转化值前,int a = (int ) (3 + 4.5);

由于使用0和1,而不是true 和false 关键字,表达式4 > 5结果是0 ,4 < 5的结果是1,在if,while等条件表达式中,非0数字也被认为是true。

赋值操作也是一个表达式,会得到一个值。v = e的值是赋值过后的v。如果i是int类型,i=72.99的值是72,而不是72.99,1 + (i = 72.99) 的值为73。程序就是求值,如果求值的过程中改变了其它东西,就是产生了副作用。(i=72.99)求值是72,但同时,它也改变了变量i的值,这就是副作用。如果一个表达式中有多个副作用? 怎么执行,需要序列点。序列点(sequence point)是程序执行过程中的一个点,在该点上,所有的副作用都必须执行完,才能进入下一步。语句结尾的分号是序列点,每一行语句中的所有副作用都执行完,才能执行下一行语句。if和while的测试条件也是序列点,只有测试条件中的所有副作用都执行完毕,才能执行if 和while语句块。逗号也是序列点,ounces++, cost = ounces * 2 ,ounces++完成后,再执行ounces * 2。逻辑表达式也是从左到右执行,&& 和 || 也是序列点,它们前面的表达式中的所有副作用,都执行完,才执行后面的表达式。子表达式不是序列点,完整表达式才是。y = (4 + x++) + (6 + x++);   4 + x++ 是子表达式,所以C不保证执行完它之后,x会加1,后面6+x++中x的值不确定,C也不保证,先执行哪一个子表达式,表达式(a+b) * (c -d )中,不知道(a+b)先计算,还是(c -d )先计算,所以不要写这样的表达式,但整个赋值表达式是完整表达式,且后面有;,都是序列点,C保证,执行下一句的时候,x会被加两次。

求值时,先看优先级,再看结合律(从左到右还是从右到左执行),最终有4个操作符(逗号,&&,||, ?: )
对整个表达式的顺序施加一定的控制,要么一个表达式执行完,再执行另外一个表达式,要么一个表达式,完全不执行。除此之外,编译器按照自己的顺序来求值表达式。

函数

C语言的函数比较特殊,函数的声明和函数的定义是分开的。函数声明就是函数接收什么参数,返回什么类型的值,也称为函数原型(签名),函数原型以;结尾,所以函数声明的格式为 返回值类型 函数名(参数列表); 函数使用之前,要先声明它。

//不要忘记后面的;分号,它表示是声明函数,而不是定义函数
void starbar(void); //void作为参数,表示函数不接受参数,void作为返回值,表示函数没有返回值
int square(int number); //函数接受一个int类型的参数,返回一个int类型的值。

函数定义就是实现函数的功能。函数返回值 函数名(参数类型 参数名,.....) {},函数定义的中函数头必须和函数原型保持一致

void starbar(void) { //函数头
    for (size_t count = 0; count < 10; count++){
putchar(
'*');
}
  printf("\n");
}
int square(int number){ //函数头 return number *number;
}

调用函数,就是函数名后面加(),如果函数接受参数,就在括号中提供参数,提供的参数的类型和个数(实参)要与函数声明中定义的参数类型和个数(形参)保持一致。如果参数需要计算,C语言并不保证,哪一个参数先计算,有可能第二个参数,比第一个参数先进行计算。

int main(void) {int num = square(2);
printf(
"2的平方 %d \n", num);
starbar();
return 0;
}

函数的调用都是按值传递,实参的值(如果实参是表达式,先求值)会被复制给形参。需要注意的是,函数的调用如果形参和实参类型不匹配,C不会报错,而是隐式地把实参转化成形参的类型,比如square('!'), '!' 会转化成int类型的33。返回值也是一样,如果返回值和函数定义的返回值类型不一样,先把返回值转换成定义的类型再返回,假设 square() 中number * number 的结果就5.23,由于返回int,函数实际的返回值是5,虽然return 5.23。不建议使用隐式的类型转化。

数组

数组就是数据类型相同的一组元素,所以声明数组时,要声明数组中元素的个数(数组长度)以及元素的类型,格式为:元素类型 数组名[数组长度],比如 int num[5]。数组长度一旦确定,就不能改了。声明的时候可以初始化,使用{} 把初始值包括起来,初始值之间用,隔开。初始化的方式也有很多

int n[5] = {32, 27, 64, 18, 95}; //显示初始化数组中的每一个值
int n[5] = {3}; //初始值列表{}中的值比数组的长度小,没有被显示初始化的元素,值为0。n[0]是3, n[1]到n[4]都是0
int n[] = {1, 2, 3, 4, 5}; //声明数组并初始化,可以不用声明数组的长度,编译器会自己计算
int arr[6] = {[5] = 212}; //指定初始化,arr[5]是212, 数组其它元素全初始化为0//下面也是指定初始化,只不过指定初始化后面还有值,[4] = 31,30,31,后面的值被用来初始化后面的值,就是[5] =30, [6]=31//如果对同一个元素初始化2次,最后面的会覆盖前面的。[1]=29 会覆盖前面的28。没有被初始化到的元素取值还是0
int days[] = {31,28, [4] = 31,30,31, [1] = 29};

C语言并没有提供操作数组的方法,length要用sizeof来计算,sizeof(数组名)/sizeof(数组第一个元素)。其他操作基本靠for循环来完成。sizeof运算符很特殊,“sizeof表达式”中的子表达式并不求值,它只根据子表达式得到它的类型,然后把这种类型所占的字节数返回。由于sizeof 表达式中的子表达式不需要求值,所以在编译时就能知道它的大小,比如sizeof n的值是20 ,sizeof n[0] 的值是4,就可以把sizeof n/sizeof n[0] 替换成常量5,这是一个常量表达式。

int main(void){int n[5] = {32, 27, 64, 18, 95};for (size_t i = 0; i < sizeof(n) / sizeof(n[0]); i++){
printf(
"element %d \n", n[i]);
}
return 0;
}

二维数组是数组的数组,首先是个一维数组,一维数组每一个元素又都是一维数组。一维数组有3个元素,每一个元素又都是长度为5的数组,int arr[3][5]。取二维数组中的元素就要指定第几个数组和数组中第几个元素,arr[1][2]就是第二个数组第三个元素。对二维数组数组的操作通常 是嵌套for循环

int main(void) {int arr[3][5];for (size_t i = 0; i < 3; i++) {for (size_t j = 0; j < 5; j++) {
arr[i][j]
= 1; //二维数组所有元素赋值为1 }
}
printf(
"Element %d \n", arr[1][2]);return 0;
}

指针

在内存中存放东西,都是有地址的,指针就是内存地址,指针变量就是存放内存地址的变量(变量的值是内存地址)。声明指针变量的方式是 (存放谁的地址)谁的类型 * 变量名。int *yPtr,yPtr就可以存储int类型变量的地址。&操作符能取到变量的内存地址(变量在内存中的位置),比如 int y = 5; &y就可以取到变量y的地址,也就可以把它赋值给一个指针变量。int *yPtr = &y;

yPtr中保存的是变量y所在的内存地址(yPtr指向y),通过yPtr也可以找到变量y,这要用到 *操作符(指针操作符),也称为解引用。当*用到一个指针变量名前面,它是解引用,找到指针指向的变量或找到变量中保存的内存地址。*yPtr 就找到了y。*yPtr就等价y。对y进行什么操作,就可以*yPtr 进行什么操作。一个变量在内存中占多个字节,第一个字节的地址就是变量的地址。如果一个变量num的占据4个字节,2000, 2001, 2002, 2003, 那么变量num的地址是2000。指针可以被初始化为NULL, 0, 表示不指向任何内容。

一维数组和指针

当数组名在一个表达式中时,它会转换成地址,数组首元素的地址。如果a是个数组,在表达式中,a和 &a[0]等价,都是地址常量,因为在程序执行的过程中,数组一旦在内存中开辟了空间,就不会变了。所以不能把数组作为一个整体,赋值给另外一个数组

int oxen[SIZE] = {5, 3, 2, 8};
intyaks[SIZE];
yaks
= oxen; /*不允许 数组名是一个常量*/ 

由于数组名会转化成地址,所以当把它传递给一个函数时,函数接收到的是数组首元素的地址,因此也就不能知道数组的长度了。在C中,操作数组的函数有两个参数,一个是地址,一个是数组的长度。地址的表示方式有两种,int sum(int *ar, int n);,一种是int sum(int arr[], int n);, 通常使用第二种,更清晰表明参数是数组

int sum(int arr[], intn) {int total = 0;for (int i = 0; i < n; i++)
total
+= ar[i]; //用数组的方式来操作指针, ar[i] 相当于 *(ar + i). 数组名虽然是一个指针,但也能通过下标的方式来操作指针 returntotal;
}

当指针指向数组元素时,指针可以做算术运算(+, -) 和比较(<, >, <=, >=,  ==, !=)运算。假设a是一个数组,p是一个指针,指向arr[i],p + j, 就是指向数组a[i + j],指针+1 加的是一个存储单元,比如double占8个字节,它就加8个字节,正好是double数组的下一个元素,这也是声明指针变量的时候,指定指针类型,要加多少个字节,获取指针变量指指向的变量的值时,要读多少个字节。

p -j 则指向 a[p - j ]

两个指针相减,则产生两个指针之间的distance,

另外一种用纯指针的方式来操作数组,

#include <stdio.h>
#define SIZE 5
int sum(int *start, int *end) {int total = 0;while (start <end) {
total
+= *start;
start
++; //指针变量的值,是可以变化的, 进行算术运算。 }returntotal;
}
int main(void) {int arr[SIZE] = {20, 40, 50, 30, 10};int *start = arr; //由于是地址, 可以赋值给指针变量 int *end = arr +SIZE;

printf(
"total: %d \n", sum(start, end));return 0;
}

start是数组首元素地址,相当于数组0的位置,start + 1是第一个元素,start + 2 是第二个元素,数组大小是SIZE,start + SIZE - 1就是数组最后一个元素的地址,但C保证start + SIZE 也是有效地址,可以用于比较大小,但不保证该地址内的元素是有效无素,所以不要取这个地址内存的值。

函数接受到的是数组的地址,所以函数中对数组的改变,也会改变函数外面的数组。如果不想改变原数组,函数参数加const修饰,const int arr[]。

指针和二维数组

二维数组,尽管在视觉上,可以看作一个行列矩阵,如下二维数组

int arr[3][4] ={
{
11,22,33,44},
{
55,66,77,88},
{
11,66,77,44}
};

可以看做

但是在内存中,它们实际上是按行存储的(row-major order)

数组名arr是数组首元素的地址,对于二维数组来说,数组首元素也是一个数组(4个int的数组),所以数组名arr是4个int元素数组的地址。arr+1 直接越过4个int,指向二维数组的第二个元素。arr[0], arr[1], arr[2] 也是一维数组,相当于一个普通的一维数组名,arr[0], arr[1], arr[2] 是对应在一维数组的首元素的地址。arr[0]+1只是指向一维数组的第二个元素。*arr相当于来到第一个数组元素(一维数组 arr[0]),*(arr+1)相当于来到数组的第二个元素(一维数组arr[1])。arr[0], arr[1] 也是一维数组,对它进行解引用,就相当于数组的第一个元素了,int类型了。变量保存的是地址,变量的值是地址,才能解引用。假设二维数组 int zippo[4][2]

如果把二维数组作为参数传递函数,怎么表示参数?如果用指针表示,int (*pt)[4], p指针变量指向了一个数组,这个数组有4个元素,每一个元素的类型是int,整个函数原型是int sum2d(int (*pt)[4], int rows); 就像一维数组一样,参数通常用数组表示法,int sum2d(int ar[][4], int rows); rows 表示数组的长度,4表示二维数组中每一个一维数组的长度,因为操作二维数组是两个for循环,它们都需要怎么终止循环。但这种做法不够通用,C99 有了变量长度数组(VLA: variable-length array)的概念,就是声明数组的时候,数组长度可以是一个变量,  因为在以前,数组的长度必须是常量。

int quarters = 4;
int regions = 5;
double sales[regions][quarters];

声明之后,数组的长度就不会变化了。再强调一下,VLA是声明的时候,长度是变量,而不是,数组长度是可变的,在C中,数组的长度一旦确定,就不会再变化了。可以用VLA来处理二位数组,int sum2d(int rows, int cols, int ar[rows][cols]);

//ar是 VLA,数组的长度是由前面的参数n决定的,因此参数的顺序非常重要
int sum2d(int rows, int cols, intar[rows][cols]){int tot = 0;for (int r = 0; r < rows; r++) {for (int c = 0; c < cols; c++)
tot
+=ar[r][c];
}
returntot;
}

数组传参时,还有一个数组字面量(复合变量)的概念,复合变量,不提前声明,使用时,直接创建,没有名字,比如声明数组复合变量

total = sum2d(2, 3, (int []){3, 0, 3, 4, 1})

函数名也是地址,一个函数声明一个指针形参,就可以接受函数。char (*p)(void),就是声明一个p指针变量,可以接受一个函数。通过指针变量来调用函数, (*p)() , 其实直接调用 pf()也可以

C语言函数调用最大的特点是,它能接受指针,所以获取函数的执行结果,并不一定非要从函数的返回值获得,而是可以使用参数获得。定义一个变量,把变量的地址做为参数传递给函数,函数内的操作改变了外部变量,可以通过函数外的变量获取函数操作的结果,函数的调用都是同步的。

字符串

C语言中没有字符串类型,用char类型的数组来表示字符串。都是char类型的数组,C是怎么知道它是字符串的?看一下字符串的存储,虽然没有字符串类型,但有字符串字面量,"Zing went the strings of my heart!"  就是字符串字面量。

当C碰到一个长度为n的字符串时,它会在内存中开辟n + 1个空间,然后把每一个元素都放入数组中,多申请的一个的1是放\0(空字符),表示字符串的结束。char数组存储字符串时,数组的长度要比实际存储的字符串的长度大1.’ char name[40] 只能存储长度为39的字符串。当然,也可以少放一点,比如

 char name[40] = "sam";

没有占用的数组元素全设置为\0(空字符)。char str[] = {'H', 'E', 'L', 'L', 'O', '\0'};  就是个字符串,char  str[] = {'H', 'E'}就是个普通char数组。字符串字面量是不可变的,由于用数组表示,字符串的值是它的第一个字符的地址。

const char *colorPtr = "blue"; //*sPtr表示它是一个指针,指向的是const char类型,也就是说不能通过指针改变它指向的变量的值。

两种形式

const char *pt1 = "Something is pointing at me.";const char ar1[] = "Something is pointing at me.";

数组形式(ar1[])会在计算机内存中开辟29个连续的内存空间,把字符串字面量复制到数组中。通常来说,字符串字面量放到静态内存中。这时字符串字面量有两副副本,一份在静态内存中,一份在数组ar1中。指针形式*pt1,只是为指针变量pt1开辟一块内存空间,然后把静态内存的字符串字面量首字符地址赋值给它。
字符串是常量,所以不能使用指针改变,但是数组形式不一样,因为数组中已经复制了一份。
In short, initializing the array copies a string from static storage to the array, whereas initializ-ing the pointer merely copies the address of the string.

C语言提供了许多处理字符串的函数,函数原型都放到了string.h头文件中。stren() 返回字符串的长度,它是size_t 类型,要用%zd 打印, 碰到 \0 就停止。strcat()拼接字符串,接受两个字符串作为参数,把第二个字符串参数复制到第一个参数字符串的末尾

#include <stdio.h>#include<string.h>

int main(void) {char flower[13] = "flower";char addon[] = "add";

strcat(flower, addon);
puts(flower);
return 0;
}

strcat() 有一个问题,就是它不会检查第一个数组的长度是否能够放得下第二个参数,因此有了strncat(), 它能限制复制第二个字符串多少个字符。strncat(bugs, addon, 13), 它会复制addon的内容给bug时,在复制的过程中,如果达到13个字符,或遇到空字符\0,它就停止复制,然后,再在新的bug字符串的末尾添加空字符\0。也就是bug数组的长度,至少是,它本身字符串的长度 + 最大13个字符 + 结束符标志符\0

int main(void) {const int BUGSIZE = 13;charbug[BUGSIZE] = "flower"char addon[] = "addddddddddddddd";
const int available = BUGSIZE - strlen(bug) - 1;
strncat(bug, addon, available);

puts(bug);
return 0;
}

strcmp() 比较两字符串的内容是否相同,如果相同返回0,如果不相同返回非0整数。strncmp()则是比较指定的前几个字符,相等返回0,不相等返回非0整数。

int main(void) {char answer[] = "Grant";char try[] = "GrAdd";int no_right = strcmp(answer, try);int right = strncmp(answer, try, 2);
printf(
"is right? %d \n", right);return 0;
}

strcpy把一个字符串复制给另一个字符串,相当于赋值操作。要确保目的地有足够的空间来存储复制的字符串。

int main(void) {char target[20];
strcpy(target,
"Hi ho!"); /*assignment for strings*/target= "So long"; /*syntax error 数组名是常量*/    
   const char * orig = "beast"; char copy[] = "Be the best that you can be.";
strcpy(copy
+ 7, orig); // 需要注意的是复制的时候,原字符串(orig)的\0也会复制到新数组 return 0;
}

strncpy(),指定复制几个字符,当然如果要复制的字符串大短,碰到了\0,复制也就停止了,也就是说strncpy 复制停止的条件是,要么碰到了\0,要么达到了字数限制。如果要复制的字符大长,复制达到字数限制,那就没有字符串的\0的地方了,所以,复制的字数限制都是比目标数组小于1, 复制完成后,再手动添加\0。

int main(void) {const int TARGSIZE = 5;charqwords[TARGSIZE];char temp[] = "Be the best that you can be.";

strncpy(qwords, temp, TARGSIZE
- 1);
qwords[TARGSIZE
- 1] = '\0';

puts(qwords);
return 0;
}

汉字如果按UTF-8编码,一个汉字占3个字节,这种字符在C语言中称为多字节字符(Multibyte Character)。printf("你好\n");相当于把一串字节(e4 bd a0 e5 a5 bd 0a 00) 写到终端,如果当前终端的驱动程序能够识别UTF-8编码就能打印出汉字,如果当前终端的驱动程序不能识别UTF-8编码(比如一般的字符终端)就打印不出汉字。也就是说,像这种程序,识别汉字的工作既不是由C编译器做的也不是由libc 做的,C编译器原封不动地把源文件中的UTF-8编码复制到目标文件中,libc 只是当作以0结尾的字符串原封不动地write 给内核,识别汉字的工作是由终端的驱动程序做的。

为了在程序中操作Unicode字符,C语言定义了宽字符(Wide Character)类型wchar_t和一些库函数。在字符常量或字符串字面值前面加一个L就表示宽字符常量或宽字符串,例如定义wchar_t c = L'你'; wcslen函数就可以取宽字符串中的字符个数. printf("%ls", L"你好\n");编译器会把它变成4个UCS编码0x00004f60 0x0000597d 0x0000000a 0x00000000 保存在目标文件中。printf的%ls 转换说明表示把后面的参数按宽字符串解释,不是见到0字节就结束,而是见到UCS编码为0的字符才结束,但是要write 到终端仍然需要以多字节编码输出,这样终端驱动程序才能识别,所以printf在内部把宽字符串转换成多字节字符串再write 出去。一般来说,程序在做内部计算时通常以宽字符编码,如果要存盘或者输出给别的程序,或者通过网络发给别的程序,则采用多字节编码。

restrict 也是是一个qualifier to a pointer type  It tells the compiler that this pointer is not an alias of anything else; the memory it points at is only referenced through the pointer itself. Writing to other pointers will not change the value it points at, and writing through the pointer will not affect what other pointers read. 所谓alias, 就是同时有两个指针指向同一个内存地址,修改一个,就会引起另外一个的变化。所以当我们给一个restrict 指针赋值时,不能是一个alias的指针。

#include <stdio.h>

void abc_restrict(int *a, int *b, int *restrict c)
{
    *a += *c;
    *b += *c;
}

int main(void)
{
    int x, y;

    x = y = 13;
    abc_restrict(&x, &y, &x); //错误的。参数是的指针a和指针c 同时指向一个内存地址
    printf("%d %d\n", x, y);
}

结构体

结构体是对一类实体的描述,有什么属性,类似于Java或JS中的类,但结构体只描述属性,不包含方法。结构体类型的定义,用struct,后面跟一个可选的tag名,然后{} 定义属性,最后以分号结尾

structbook {char title[51];char author[21];floatvalue;
}; // 以分号结尾

struct book library; 声明了结构体变量libaray。声明的时候可以初始化,大括号中的数据依次赋值给结构体中成员,如果Initializer中的数据比结构体的成员多,编译器会报错,如果Initializer中的数据比结构体的成员少,未指定的成员将用0来初始化,

struct book library = {"The Pious Pirate and the Devious Damsel",
"Renee Vivotte", 1.95};

c99 初始化

     struct book surprise = {.value = 10.99};
struct book gift = {.value = 25.99,
.author = "James Broadfool",
.title = "Rue for the Toad"};

声明了结构体变量,怎么访问它的成员,使用点号, 比如surpise.value;   指针与结构体,struct book  * plibrary;  plibaray = &libray. 如果使用指针获取成员 ,一种是解引用(*library).value,一种是使用->, plibaray ->value. typedef 给一个类型定一个别名。

结构体类型和基本类型(int , double)使用方式一样,结构体可以赋值给另外一个结构体变量。struct book anotthr = library; 可以作为参数传递给函数,也可以作为函数的返回值。可以取结构体的地址,函数传参的时候,有两种方式,一种是把整个结构体,复制一份,传递过去,一个是把结构体指针传递过去,结构体也能作为函数的返回值。 结构体字面量  (struct book) {"The Idiot", "Fyodor Dostoyevsky", 6.99} , &(struct book) {"The Idiot", "Fyodor Dostoyevsky", 6.99} 则是取其地址。

内存对齐:是由于硬件方面的原因,cpu一次从内存中读取一定长度的字节,64bit操作系统,就是一次从内存中读取64bit。如果,内存放置诶只不对,一次读取不完,cpu要读取两次。通常来说,硬件更喜欢,你把元素放到 元素大小的倍数的位置上。比如一个int,4个字节,它放置到的内存位置最好是4的倍数。比如一个double 8个字节,最好把double类型的元素放置8的倍数的内存地址上。C!! 有一个alignof(), 就是告诉你类型的对齐的限制,它接受一个类型作为参数,返回地址对齐的倍数,所以对于一个struct来说,它实际占的内存大小,可能从比它本身的成员相加要大。当一个结构体数组时更是如此,数组的第一个结构体肯定是align的,那第二个怎么排列? 有可能要在第一个结构体和第二个结构体之间插入空间,也可能是第一个结构体最后一个元素后面插入空间。

union 联合类型

union hold {
int digit;
double bigfl;
char letter;
};

它所有的成员公用一块内存,它或是存一个int, 或是存一个double,或是存一个char,在同一块内存地址存储不同数据类型,同一时刻,union只存储一个值。当创建union类型的变量时,编译器会分配足够的空间,以便它能存储union类型中占用最大字节的类型。

union hold valA;  
valA.letter = 'R';
union hold valB = valA; //initialize one union to another
union hold valC = {88}; //initialize digit member of union
union hold valD = {.bigfl = 118.2}; //designated initializer

枚举类型
,主要是变量的取值,就只能取这么多,符号常量,主要是限定作用。

enumJar_Type { CUP, PINT, QUART };
enum Jar_Type milk_jug; // 声明变量

实际上变量作为整数进行存储,符号常量也是作为数字进行存储,CUP是0,PINT 是1,在某些情况下,可能需要特定的数字来表示常量符号

enum Jar_Type { CUP=8, PINT=16, QUART=32};

动态分配内存(stdlib.h)

malloc()函数,在内存中分配参数指定的空间,然后返回这块内存的首字节地址,因此把它赋给一个指针变量,就可以通过指针访问这块内存。malloc()返回的是void类型(通用类型)的指针,因为它不知道要存储什么类型的数据,所以它不能返回指向普通类型指针,比如,int 或char, 而是返回 void * 类型的值。把void类型的指针赋值给任意类型的指针完全不用考虑类型匹配问题,但通常会强制类型转换一下。如果malloc()分配内存失败,则返回空指针。

double *ptd;
ptd = (double *)malloc(30 * sizeof(double));
if(ptd == NULL) {//分配失败} // ptd[0], ptd[1],可以用数组的方式,使用内存

free(),接受malloc()返回的地址,释放malloc分配内存。手动分配的内存,需要手动释放, C没有垃圾回收机制。calloc() 也是手动分配内存,calloc(100, sizeof(int)), 只不过它会初始化为0,其他的都和malloc一样。

relloc: 增加或减少以前分配的空间,所以它的第一个参数是以前用molloc等分配的空间的地址(指针),第二个参数是大小,newp = realloc(p, ..), 如果 newp不是null,则p指针就不能再用了,以前使用p的地方都要换成newp。relloc的工作原理:调用malloc 分配内存,然后旧的内容复制到新内存中,如果分配的新内存小,新内存满了就不复制了。如果分配的新内存大,复制完旧的内容后,还有剩余,剩余的部分不会初始化。分配成功,就会调用free把旧内存释放掉,但如果分配失败,会返回null,但旧的的内存仍然存在。以下是错误的用法

int main(void)
{
    int size = 10;
    int *p = (int *) malloc(size * sizeof(int));

    for(int i = 0; i< size; i++) {
        p[i] = i;
    }

    size += 10;

    // 有错误
    if((p = realloc(p, size)) == NULL) {
        return NULL;
    }

    printf(" %d \n", p[5]);
    return 0;
}

如果relloc 失败,p就是null,但是旧的p指向的内存并没有释放,导致内存泄漏。所以要声明两个变量,一个是保存malloc返回的指针,一个是保存realloc返回的指针, 把有错误的代码改成

//...
void *p2;if((p2 = realloc(p, size)) ==NULL) {
free(p)
returnNULL;
}
p
=p2

printf(
"%d \n", p[5])//...

当有了动态分配内存,结构体有了灵活数组成员(柔性数组),声明结构体的时候,至少有两个成员,最后一个成员是数组,其它成员是什么都行,数组的并不需要数组长度。数组有个特性,它不会立即存在,只有经过适当的操作之后,这个数组才能正常使用

structflex {
intcount;
doubleaverage;
double scores[]; //灵活数组成员
};

如果声明一个struct flex变量,你不能使用 scores, 因为并没有为它分配内存空间。事实上,也不应该这么用。我们应该声明一个struct flex指针变量(结构体指针),然后使用malloc分配足够的空间给普通的结构体成员和灵活数组,来存储 struct flex的正常类型的内容和柔性数组的内容(use malloc() to allocate enought space for the ordinary contents of struct flex plus any extra space you want for the flexiable array member),比如 想让scores 代表5个double值的数组,然后使用malloc()来分配空间

struct flex *pf; 
//分配空间给结构体和数组
pf = malloc(sizeof(struct flex) + 5 * sizeof(double));

分配了count 和average,然后5个数组,然后数组和结构体成员都可以使用了

pf->average = 12.5;
pf->scores[2] = 10.2;

当然,使用柔性数组的struct有限制,比如不能使用赋值的方式,进行copy,因为这会copy struct 普通类型,要使用memcpy(). 当然,函数传参的时候,不要使用struct 类型,要使用指针,原因和赋值一样。最后是柔性数组的struct不要做为数组元素和其它struct 类的成员

多文件程序

编写应用程序不可能只有一个文件,多个文件就涉及到共享,隐藏等问题,C语言没有模块的概念,相对应的是变量有一个链接属性,内部链接的标识符只能在本文件中使用。如果另外一个文件中有同名的变量,它们被认为是不同的变量。外部链接的定义标识符,可以在程序中的其它文件中使用,虽然标识符是在这个文件中定义的,但是它完全可以在项目中其他文件中使用。在文件作用域或全局作用域的变量和函数默认都是外部链接,在变量和函数前面加static,就变成了内部链接。只有文件作用域的定义变量和函数才有链接属性,块级作用域没有链接属性。

怎么在一个文件中使用另一个文件中的定义的标识符,使用变量和函数都要提前先声明的,extern来声明变量和函数。extern int i; 千万不要 extern int i = 0; 它相当于 int i = 0; extern void sum(int a, int b); 由于函数声明默认是extern 的,函数声明前可以不加extern。extern 就是告诉编译器去其他地方找标识符的定义,因此,extern 声明不占内存空间,也意味着在整个项目中,标识符只能有一个可见的定义。 函数声明只是声明了一个标识符,函数定义才是在内存中开辟空间,保存起来。变量的声明也是,它也只是声明了一个标识符号,只有赋值,才在内存中开辟空间,算是定义。但对于external的变量,只识别前31个字符。也最好不要使用以_下划线开头的变量名,以免和库文件有冲突。但这样使用有一个问题,就是如果10个文件中都用了一个函数,函数的声明就要在10个文件中都写一遍,容易出错。由于就有了头文件,把函数声明放到头文件中,然后在使用函数的文件中包含头文件。在同一个目录下,建立stack.h, stack.c, calc.c

include指令有两种形式,#include <文件名>和#include "文件名"。#include <文件名> 引用C自带的头文件,#include "文件名" 是引用自定义的文件,当然文件名中可以包含路径,最好使用相对路径,#inlucde "utils.h" 或#include "../header/untils.h". 除了调用函数的文件中引用头文件,还要在定义函数的文件包含头文件,确保原型和定义保持一致。头文件中也可以包含其它头文件

#include <stdbool.h>
void make_empty(void);
bool is_empty(void);
void push(inti);
int pop(void);

这就带来了一个问题,如果一个头文件被包含多次,并多次编译,编译器可能会报错。

需要对头文件进行保护,把头文件的内容放到#ifndef 和#endif 之间

#ifndef STACK_H#define STACK_H#include<stdbool.h>
void make_empty(void);bool is_empty(void);void push(inti);int pop(void);#endif

当这个头文件被第一次引入的时候,宏STACK_H是没有被定义的,所以预编译器会允许#ifndef 和#endif 之间 的内容存在。但是当这个文件以后再被引用时,预处理器会把#ifndef 和#endif 之间的内容删除。宏的名字最好和头文件的名字一致。

那怎么编译和运行多程序文件?如果有三个文件 justify.c包含main, line.c读取用户输入的内容,word.c 用来处理文件,line.h和word.h分别用来定义头文件。现在大部分的编译器,只用一步,就可以构建整个程序 gcc -o justify justify.c line.c word.c. 但这会有几个问题,一个是文件名大多,一个是重复编译, 为了更简单地构建大型程序,使用makefile,  一个包含构建程序必要信息的文件。makefile 不仅列出了项目中使用到的源文件,还描述它们之间的依赖关系,比如line.c 包含line.h,我们就可以说line.c依赖line.h,因为line.h发生变化,需要重新编译line.c. makefile文件的文件名就是makefile或Makefile,  在项目根目录新建makefile文件

justify:  justify.o word.o line.o
gcc
-o justify justify.o word.o line.o #tab键开始

justify.o: justify.c word.h line.h
gcc
-c justify.c

word.o: word.c word.h
gcc
-c word.c

line.o: line.c line.h
gcc
-c line.c

有四组,每一个组称为一个规则。每一规则的第一行给了一个目标文件,后面是它的依赖。第二行是执行的命令,由于依赖文件发生变化,目标文件需要重新构建,所要执行的命令。比如第一个规则:justify是目标文件,它依赖justify.o, word.o, line.o; 如果三个文件中的任何一个发生变化,justify需要重新构建。第二行就是重新构建要执行的命令。注意命行所在行要以tab 开始,不是空格。

创建了makefile文件,使用make命令来执行构建。make 命令执行的时候,它自动在当前目录查找该文件。make target 来执行命令,target 就是在makefile文件中定义的target,比如make justify. 如果不指定target, make 命令将会构建第一行规则的target。

C语言的抽象数据类型,比如栈的实现,头文件如下

#define STACK_SIZE 100typedef structstack
{
intcontents[STACK_SIZE];
inttop;
} Stack;

void push(Stack *s, inti);
void pop(Stack *s);

但Stack 并没有很好的封装性,在main 函数中 声明Stack s1, s1.top 是可行的,实际上,对于栈的操作来说,我们只想使用函数来操作栈。当然C语言本身也没有提供很好的封装,只有一个办法,不完整类型。比如struct stack;, 只告诉编译器,stack是一个结构体tag,但并没有描述结构体成员,因此结构体的定义需要在别的地方完整(stack.c 实现上), 也是因为类型是不完整的,所以不能声明变量,但可以声明指针,typedef struct stack * Stack; 但仍然不能使用 -> 获取它的成员,由不完整,就要多一个create 创建栈的方法,

#ifndef STACK_H
#define STACK_H#include <stdbool.h>typedef struct stack_type *Stack;

Stack create(void);
voiddestroy(Stack s);
void push(Stack s, inti);
intpop(Stack s);

#endif

stack,c 栈的一个简单实现

#include <stdlib.h>#include "stack.h"

#define STACK_SIZE 100

struct stack_type // 头文件中不完整类型的定义
{
intcontents[STACK_SIZE];
inttop;
};

Stack create(void) {
Stack s = malloc(sizeof(structstack_type));
if (s == NULL) exit(0);
s->top = 0;
}

voiddestroy(Stack s) {
free(s);
}

void push(Stack s, inti) {
s->contents[s->top++] =i;
}

intpop(Stack s) {
return s->contents[--s->top];
}

在main 函数中调用的时候

#include <stdio.h>#include "stack.h"

int main(void) {
Stack s1;
intn;

s1 =create();
push(s1, 1);
n =pop(s1);
destroy(s1);
}
 

C语言声明

读懂C语言的声明,看的是优先级。先找到声明的变量,然后按以下优先级方式来拚读。1,如果变量在小括号内,小括号是一个整体。2, 再看变量后面的操作符,如果是小括号表示是一个函数,如果是中括号表示是一个数组,3,再看变量前面的操作符,* 表示是一个指针变量,指向什么。如果在过程中遇到了const 和 volatile限定符,它们在类型(int 等)旁边,它应用到类型上,如果不在类型前面,就就用到它左边的 *

比如:char * const * (*next)();  next 是声明的变量,它在小括里面,和* 在一起,表是它是一个指针变量,指向什么,再看next变量后面,也是一个小括号,表示函数,这个函数没有任何参数,函数要返回什么,就是next变量前面的 char * const *。也就是说,next 变量是一个指针变量,指向一个没有参数,但是返回值是char * const *的函数。char * const * 的意思是, *表示指针,指向 char * const(不变的或只读的指针 指向char)  . 声明的时候,要注意,函数不能返回函数,也不能返回数组,数组中也不能函数,这些情况下,都需要指针。

文件操作:


C把每一个文件都看作是有序字节流,每一个文件都以文件结束符(end-of-file marker)作为结尾,如下图所示:

所以I/O仅仅是字节的流动,要么文件到程序,要么从程序到文件。文件到程序就是输入,程序需考虑正确地解释数据的字节。程序到文件就是输出,程序需要考虑如何创建正确的输出字节数据。无论读写,都要先打开文件。fopen()打开一个文件,打开成功,返回一个FILE 结构体地址,FILE结构体就包含程序要处理的文件的信息,所以把地址赋值给一个File类型的指针变量,操作指针变量,就是操作文件。如果打开失败,就返回空指针NUll。fopen() 第一个参数是字符串,要打开的文件名(文件路径),第二个参数是以什么方式打开,有 "r","rb","w","wb","a"

"r":以只读模式打开文件,如果文件不存在,返回NULL。"w":以写模式打开,如果文件存在,就清空文件内容,如果不存在,就创建文件。"a", 也是以写模式打开文件,不过,文件存在的时候,它会在文件末尾追加内容,而不是清空文件内容,如果文件不存在,则创建文件

"r+":如果文件存在,打开文件后,光标放到文件的起始位置,如果文件不存在,返回NULL,因此,它既不会删除文件内容,也不会创建文件。 当文件以r+打开时,文件可读,可写,但有特殊的规则,除非读到文件的末尾,否则不能从读切换到写,如果非要切换,必须调用文件位置的函数。调用写函数后,也不能从写切换到读,除非调用位置函数或fflush()函数。文件位置函烽:fseek, fsetpos, ftell, rewind. 光标在文件的什么地方。rewind() 把文件位置设置到文件起始位置

"w+", 打开文件,文件可读写,但如果文件存在,它会把文件内容清空,当文件不存在,它会创建文件,无论如何,先做读取操作是没有意义的,可以先做写操作,写完之后,再调用文件位置函数,对刚才写的内容进行读取。

"a+": 打开文件,文件可读写。如果文件存在,光标放到文件位置,按理说,此时读是没有办法读取到内容的,在Mac是确实是这样,但是Ubuntun下面,它是可以先进行读取操作的,把整个文件都读完。可以直接进行写操作,进行写操作的时候,a+只会在文件的末尾i写入内容,称动光标,对写入操作无效。写入操作完成后,再调用位置函数,设置光标,进行读取操作。

当用w+或a+来打开文件时,它可以读写。如果先写再读,写完之后,光标已到了文件末尾,这时候,再去读,读不到什么内容,所以在读之前要重置光标,rewind(fp), 将光标放到文件的起始位置。

rb, wb, a+b, 则是以二进制打开文件。C11 加了x模式,wx wbx,  如果打开存在的文件,会打开失败。

打开文件后,调用读取函数,会导致一块数据从文件复制到buffer中,buffer的大小通常是512 字节或它的倍数。除了填充buffer,初始的函数调用还设置fopen创建的数据结构的值,尤其是设置在stream中的当前位置,和复制到buffer中的字节数。在数据结构和buffer初始化后,读取函数从buffer中读取请求的数据,读取的过程中,文件位置指示器设置为指向最后读取的字符后面的字符(the file position indicator is set to point to the character following the last character read)。再次调用读取函数,会从上一次读取结束的位置读取。当读取函数发现它已经读取完了buffer中的所有字符,它请求下一块数据从文件中复制到buffer中,通过这种方式,读取函数可以读取整个文件的内容到文件末尾。当读取函数 读到最后一块buffer的最后一个字节,它设置EOF标志为true, 再次调用读取函数,就会返回EOF. 相同的方式,写函数也是写到buffer中,当buffer 满了,数据会复制到文件中。

如果是以文本模式打开的文件,要用文件I/O,比如 fgets(), fgetc(), fscanf()。 如果是以二进制文件方式,打开,要要使用fread 和fwrite

打开文件后,

fgetc(File 指针)可以从文件中读取一个字符, 返回int类型(代表字符),如果读到文件末尾,就返回EOF,如果读取过程中发生错误,也会返回EOF。feof()和ferror() 函数主是用于区分这两种情况,feof()读取到文件结尾,返回非0,否则返回0。如果读和写出现错误,ferror返回非0,否则返回0。

读取字符放到哪里去呢,那就用到fputc, 它的第一个参数就是字符,第二个参数就是要放置的位置。要放置的位置,就是以写模式打开的文件,如果fputc成功,返回当前写入的字符,如要失败,返回EOF。

fclose是关闭文件(必要时,刷新缓冲区),它的参数就是打开的文件的指针。对于比较正式的程序,应该检查是否成功关闭文件,如果关闭成功,fclose返回0,否则返加EOF。

fgets(),第一个参数,就是读取字符串到哪里,第二个参数就是限制读取多少个字符,如果参数的值为n,它就会只读取n-1个字符,当然,如果不到n-1,读取到了换行符,它也会停止。也就是说,fgets() 读取停止的条件是,要么读取到最大长度,要么读取到了换行符。注意读取到换行符时,它不会舍弃,读取完成后,再在读取到的字符串后面 加'\0', 存到字符串中。加'\0',表示字符串的结束,这也是第二个参数是n,fgets()读取n-1个字符的原因。第三个参数就是从哪里去读,如果从终端中去读,就是stdin。  fgets()返回一个字符指针,值和第一个参数地址一样。如果读取失败,或读取到end-of-file,就会返回空指针NULL。fputs(),它的第一个参数是,要写的内容,第二个是写到哪里,屏幕就是stdout. 它不会换行。

①fgets:从文件中读取一行数据存入缓冲区(fgets遇到回车才会结束,不对空格和回车做任何转换就录入到缓冲区,结束后再往缓冲区写多一个\0,所以它是读一行数据)  ②fscanf:从文件中读取一段数据存入缓冲区(fscanf遇到空格或回车就结束,它会把空格或回车转换为\0,所以它是读一小段数据)。假设复制 一个文件,

我爱你小白 开玩笑
哈哈
开玩笑

fgets会原样输出到目标文件

fscanf() 则会返回

fscanf() is a field oriented function and is inappropriate for use in a robust, general-purpose text file reader.

intmain()
{
FILE
*fp = fopen("test.txt", "r");if (fp ==NULL)
{
perror(
"打开文件时发生错误");return (-1);
}
const int BUFF_SIZE = 51;charbuf[BUFF_SIZE];int line = 0;while (1)
{
if ((fgets(buf, BUFF_SIZE -1, fp)) ==NULL) {break;
}
if(buf[strlen(buf) -1] == '\n') {
line
++;
}
}
if(feof(fp))
printf(
"\n End of file reached. line = %d \n", line);elseprintf("\n Something went wrong.");

fclose(fp);
return (0);
}

feof()函数还能判断一个二进制文件有没有到达文件末尾,比如,读一个mp3文件,fgetc, 和fputc 也能读取二进制文件

intmain()
{
FILE
*source;
FILE
*target;//复制source 到target source = fopen("a.mp4", "rb");
target
= fopen("b.mp4", "wb");if (source == NULL || target ==NULL)
{
perror(
"打开文件时发生错误");return (-1);
}
intc;while (( c = fgetc(source)) !=EOF)
{
fputc(c, target);
}
if(feof(source))
printf(
"\n End of file reached.");elseprintf("\n Something went wrong.");

fclose(source);
fclose(target);
return (0);
}

fread() 函数用来从指定文件中读取块数据。所谓块数据,也就是若干个字节的数据。fread() 的原型为:

size_t fread ( void *ptr, size_t size, size_t count, FILE *fp );

fwrite() 函数用来向文件中写入块数据,它的原型为:

size_t fwrite ( void * ptr, size_t size, size_t count, FILE *fp );
  • ptr 为内存区块的
    指针
    ,它可以是数组、变量、结构体等。fread() 中的 ptr 用来存放读取到的数据,fwrite() 中的 ptr 用来存放要写入的数据。
  • size:表示每个数据块的字节数。
  • count:表示要读写的数据块的块数。
  • fp:表示文件指针。
  • 理论上,每次读写 size*count 个字节的数据。

返回值:返回成功读写的块数,也即 count。如果返回值小于 count:

  • 对于 fwrite() 来说,肯定发生了写入错误,可以用 ferror() 函数检测。
  • 对于 fread() 来说,可能读到了文件末尾,可能发生了错误,可以用 ferror() 或 feof() 检测。

fwrite(a, sizeof(a[0]), sizeof(a) /sizeof(a[0]), fp )

intmain()
{
FILE
*h = fopen("file.txt", "a");char *line = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n";for (int i = 0; i < 1000; i++)
{
if (fwrite(line, 1, strlen(line), h) !=strlen(line))
{
perror(
"Could not append line to file");//Exception Handling exit(1);
}
}
if (fclose(h) != 0)
{
perror(
"Could not close file");
exit(
1);
}
return (0);
}

fread 也可以用来读文件

intmain()
{
//File pointer FILE*filePointer;//Buffer to store the read data char buffer[100];//"g4g.txt" file is opened in read mode filePointer = fopen("g4g.txt", "r");//Data is read from the file into the buffer//sizeof(buffer) specifies the size of each element to//be read 1 is the number of elements to read//filePointer is the file to read from while (!feof(filePointer)) {

fread(buffer,
sizeof(buffer), 1, filePointer);//Print the read data printf("%s", buffer);
}

fclose(filePointer);
return 0;
}

The fseek() function enables you to treat a file like an array and move directly to any partic-ular byte in a file opened by fopen().  fseek()第一个参数的fopen()打开的文件(使用rb打开), 第二个参数是从start point开始的偏移量,它是long类型,第三个参数就是标识start point。三个选项SEEK_SET(文件开始位置), SEEK_CUR(当前位置,也就是光标所在位置),SEEK_END(文件结束位置)

fseek() 近回0表示正常,fseek() 返回-1 表示不常. ftell()则表返回文件的位置,从文件开头到光标所在位置的字节数,是个long类型。

文件的读写都是缓冲区, 每个打开的文件分配一个I/O缓冲区以加速读写操作,通过文件的FILE 结构体可以找到这个缓冲
区,用户调用读写函数大多数时候都在I/O缓冲区中读写,只有少数时候需要把读写请求传给内
核。以fgetc / fputc 为例,当用户程序第一次调用fgetc 读一个字节时,fgetc 函数可能通过系统调用
进入内核读1K字节到I/O缓冲区中,然后返回I/O缓冲区中的第一个字节给用户,把读写位置指
向I/O缓冲区中的第二个字符,以后用户再调fgetc ,就直接从I/O缓冲区中读取,而不需要进内核
了,当用户把这1K字节都读完之后,再次调用fgetc 时,fgetc 函数会再次进入内核读1K字节
到I/O缓冲区中。I/O缓冲区也在用户空间,直接
从用户空间读取数据比进内核读数据要快得多。另一方面,用户程序调用fputc 通常只是写到I/O缓
冲区中,这样fputc 函数可以很快地返回,如果I/O缓冲区写满了,fputc 就通过系统调用把I/O缓冲
区中的数据传给内核,内核最终把数据写回磁盘。有时候用户程序希望把I/O缓冲区中的数据立刻
传给内核,让内核写回设备,这称为Flush操作,对应的库函数是fflush,fclose函数在关闭文件
之前也会做Flush操作

泛型函数操作数组:Generic functions are functions that will work on any type。 In C, generic types mean voidpointers. If a function can work
on more than one type, it will take one or more
void pointers asarguments.
you are always allowed to
cast data to character pointers to
get the address of their first byte, so we can cast ourvoid pointers to char pointers and manipulate the underlying data that way. Then, ifyou want to move to an address at the right distance, you must multiply the number of
elements you wish to move with the size of the objects. 当你在操作void 指针的时候,你并不知道 ,指针指向具体数据类型,指针操作无法进行,当你在调用一个void指针的函数时候,可以把要操作的数据类型的size传给它。
void reverse(void *array, int n, intsize)
{
if (n <= 0) return; //avoid right underflow char *left =array;char *right = left + size * (n - 1);chartmp[size];while (left <right) {
memcpy(
&tmp, left, size);
memcpy(left, right, size);
memcpy(right,
&tmp, size);
left
+=size;
right
-=size;
}
}
不能解引用一个void类型。当交换的时候,使用memory copy
 

编译和链接细节

对于C 语言来说,每一个C源文件都会经过编译器的单独处理,生成自己的obj文件,也就是说,有10个 .c文件,就会编译成10个.obj文件。头文件不需要单独编译,当包含头文件的文件编译时,它会自动编译头文件。然后,这些 .obj文件,再加上链接库,经过链接器链接,捆绑在一起,最终生成一个可以执行的文件或程序。编译又分为三个阶段:预处理,编译,汇编。预处理命令 gcc -E test.c -o test.i   .i 文件就是预处理后生成的文件。它主要做到事情就是

#include引入的头文件中的所有内容都插入到了 test.i 文件中了。 只是把头文件中所有内容都拷贝到了.i 文件。

注释删除(使用空格替换注释)。

#define 替换,

#define 定义标识符, #define MAX 30 结尾处一定不要加分号。 预编译阶段或预处理阶段,就是纯文本替换。

编译命令 gcc -S test.i  生成test.s文件(汇编代码) 把C代码编译成汇编代码,它就是语法分析,词法分析,语义分析 符号汇总(main, printf 等函数都会保留到汇编代码中)。 汇编命令 gcc -c test.s 生成test.o, 生成 .o二进制文件,主要是形成符号表。因为每一个源文件都会生成它自己的汇编文件,每一个源文件中都有自己的函数,也就是说每一个汇编文件中都有自己定义的函数,也就是有自己的符号。这些函数或者符号,可能被另一个C文件调用,也就是说,在另一个C文件(汇编文件)中,它也有一个相同的函数名符号。比如test.c 中调用 add.c 中的add() 函数,test.c生成test.s, 它里面会用两个符号,main, add。 add.c会生成 add.s, 它里面会有 add符号。 每一个函数都会有地址,汇编阶段,就是把符号和它对应的地址放到一张表里面。add.s 生成add.0 就会生成一张符合表。

这里要注意的是add 对应的地址并不是真实的地址,因为,它是调用的外部函数,在test中,只是声明了函数。

链接: add.o 和test.o 进行链接,要合并段表。.o文件有自己的格式,文件中的内容都分段存在的(ELF 格式)。进行链接的时候,就是把每一个文件中每一段进行合并。最终生成的可执行文件也是ELF格式。还有就是符号表的合并和重定义,因为每一个文件都有自己的符号表,最主要的是联系在一起。因为有些表中的地址是无效的,要用真实的地址,进行替换,函数的调用尤其如此,最终生一张都是真实地址的表。

add是真实的地址,main 函数在调用add的时候,就会找的到。