2023年4月

讲在前面,chatgpt出来的时候就想过将其利用在信息抽取方面,后续也发现了不少基于这种大语言模型的信息抽取的论文,比如之前收集过的:

接下来继续介绍另一篇论文。

GPT-NER:通过大型语言模型的命名实体识别

GPT-NER: Named Entity Recognition via Large Language Models

https://arxiv.org/pdf/2304.10428v1.pdf

https://github.com/ShuheWang1998/GPT-NER

Part1

前言

为什么使用大语言模型在NER上的表现仍然明显低于普遍的基线?

由于NER和LLMs这两个任务之间的差距:前者本质上是一个序列标记任务,而后者是一个文本生成模型。

怎么解决上述的问题呢?

  • GPT-NER通过将序列标签任务转换 为一个可以被LLMs轻松适应的生成任务来弥补这一差距,例如, 在输入文本Columbus是一个城市中寻找位置实体的任务被转换为生成文本序列@@Columbus##是一个城市,其中特殊标记@@##标志着要提取的实体。
  • 为了有效地解决LLMs的幻觉问题,即LLMs有一个强烈的倾向,即过度自信地将NULL输入标记为实体,我们提出了一个自我验证策略,即提示LLMs询问自己所提取的实体是否符合标记的实体标签。

Part2

介绍

GPTNER遵循语境学习的一般范式,可以分解为三个步骤:

  • (1)构建提示:对于一个给定的输入句子X,我们为X构建一个提示(用Prompt(X) 来表示);
  • (2)将提示输入到大语言模型得到生成的文本序列W = {w1 ,..., wn };
  • (3)将文本序列W转化为实体标签序列,以获得最终的结果。

如图所示:第一句话:你是一个优秀的语言学家;第二句话:任务是从给定的句子中标记xxx实体。接下来是一些例子,然后给树了一些例子。最后再输入自己想要提取实体的句子得到结果。很容易发现,
每次只能提取一种实体
,因此要提取出所有的实体,必须遍历实体列表。例如GPT-3,对提示的长度有 一个硬性的限制(例如GPT-3的4096个tokens)。鉴于这种有限的标记数量,我们不可能在一个提示中包括对所有实体类型的描述和演示。

1
怎么提供实例样本?

如图所示:

  • 1、一个已经训练好的ner模型提取训练数据中的实体,并为每一个实体构建(实体,句子)对。
  • 2、将句子输入的模型中并获取实体的表示。
  • 3、通过knn找到和实体向量最接近的几个邻居,将得到的句子视为样例。

2
怎么进行自我验证?

Prompt:
I am an excellent linguist. The task is to label location entities in the given sentence.
Below are some examples.
Input:Columbus is a city
Output:@@Columbus## is a city
Input:Rare Hendrix song sells for $17
Output:
GPT-3 Output:
Rare @@Hendrix## song sells for $17

过度预测是指将不是实体的预测为实体。如上面的例子:Hendrix被识别为一个location实体,这显然是不对的。
自我验证策略
:给定一个由LLM提取的实体,我们要求LLM进一步验证该提取的实体是否正确,用是或否回答。比如:

“The task is to verify whether the word is a location entity extracted from the given sentence”
(1) “The input sentence: Only France and Britain backed Fischler’s proposal”,
(2) “Is the word "France" in the input sentence a location entity? Please answer with yes or no”.
(3) Yes

同样的,也是根据之前的策略选择样例。

Part3

实验

  • 模型:GPT-3 (Brown et al., 2020) (davinci-003)
  • 最大长度:512
  • 温度:0
  • top_p:1
  • frequency_penalty:0
  • presence_penalty:0
  • best_of:1

总结一下,利用大语言模型进行信息抽取,大多数都是采用这种类似问答的方式进行的,也就是分为多个步骤。

一、前言

先看一下Swift标准库中对
CustomStringConvertible
协议的定义

public protocol CustomStringConvertible {
    /// A textual representation of this instance.
    ///
    /// Calling this property directly is discouraged. Instead, convert an
    /// instance of any type to a string by using the `String(describing:)`
    /// initializer. This initializer works with any type, and uses the custom
    /// `description` property for types that conform to
    /// `CustomStringConvertible`:
    ///
    ///    struct Point: CustomStringConvertible {
    ///        let x: Int, y: Int
    ///
    ///        var description: String {
    ///            return "(\(x), \(y))"
    ///        }
    ///    }
    ///
    ///    let p = Point(x: 21, y: 30)
    ///    let s = String(describing: p)
    ///    print(s)
    ///    // Prints "(21, 30)"
    ///
    /// The conversion of `p` to a string in the assignment to `s` uses the
    /// `Point` type's `description` property.
    var description: String { get }
}

从声明中我们可以看到协议中只包含了一个
description
的只读属性 ,而且通过协议命名也可以窥探到它的作用 Custom+String+Convertible (所作用的类型去自定义String的转换)

实现
CustomStringConvertible
协议类似于在Objective-C中重写
description
方法, 可用于:

  • 自定义作用类型的print输出
  • 作用的类型可自定义转换成String

如标准库中给的示例,拿出来分析一下:

struct Point: CustomStringConvertible {
    let x: Int, y: Int

    var description: String {
        return "(\(x), \(y))"
    }
}

let p = Point(x: 21, y: 30)
let s = String(describing: p)
print(s)


// Prints "(21, 30)"

上例中结构体Point 实现了CustomStringConvertible协议, 完成了description属性的实现, 返回自定义字符串 "((x), (y))"。 接着使用String类型的
String(describing: p )
初始化方法完成了 Point结构体转成指定String类型格式的转换。

通过上面的介绍,我们基本上了解了CustomStringConvertible协议的用法, 接下来介绍几种使用场景。

首先要知道的是 -- 在Swift中可以实现协议的类型有
结构体



枚举
。 也就是说只有结构体、 类、 枚举等类型都可以实现CustomStringConvertible协议

二、使用场景

1. 整型类型的枚举使用

enum AudioStatus: Int {
	case stopped = 0, playing, recording, interruptionPlaying, interruptionRecording
}

如果在使用枚举时,除了需要访问枚举的
整型值
外,还需要可以方便的输出每个枚举对应的字符串类型的状态。 那么在这种场景下,通过
extension
扩展枚举,并实现
CustomStringConvertible
协议将会很合适

extension AudioStatus : CustomStringConvertible {
    
    var description: String {
        switch self  {
        case .stopped:
            return "Audio: Stopped"
        case .playing:
            return "Audio: Playing"
        case .recording:
            return "Audio: Recording"
        case .interruptionPlaying:
            return "Audio: interruptionPlaying"
        case .interruptionRecording:
            return "Audio: interruptionRecording"
        }
    }
}

使用:

let status:AudioStatus = .stopped
let audioName = String(describing:status)  //取整型枚举对应的 字符串值
print(“audioName:\(audioName)”)

2. Class类型的使用

定义一个类的话, 当我们使用print 时候并不会输出类中的变量

class Wheel {
    var spokes: Int = 0
    var diameter: Double = 0.0
    
    init(spokes:Int = 32,diameter:Double = 26.0) {
        self.spokes = spokes
        self.diameter = diameter
    }
    
    func removeSpokes() {
        spokes = spokes > 0 ? spokes-- : spokes
    }
}

var wheel = Wheel(spokes: 36,diameter: 29)
print(wheel)
/**
 *  "Wheel\n"
 */

如果想要改变 print 的输出结果,我们需要让类遵守这个协议,最好用 extension扩展

extension Wheel: CustomStringConvertible {
    var description: String {
        return "wheel has \(spokes) spokes"
    }
}
var wheel = Wheel(spokes: 36,diameter: 29)
print(wheel)
/**
 *  "wheel has 36 spokes\n"
 */

如果想了解更多内容,可以参见专栏
《ios开发你需要知道的》

一、引子·功能需求

我们创建了一个 School 对象,其中包含了教师列表和学生列表。现在,我们需要计算教师平均年龄和学生平均年龄。

//创建对象
School school = new School()
{
    Name = "小菜学园",
    Teachers = new List<Teacher>()
    {
        new Teacher() {Name="波老师",Age=26},
        new Teacher() {Name="仓老师",Age=28},
        new Teacher() {Name="悠老师",Age=30},
    },
    Students=  new List<Student>()
    {
        new Student() {Name="小赵",Age=22},
        new Student() {Name="小钱",Age=23},
        new Student() {Name="小孙",Age=24},
    },
    //这两个值如何计算?
    TeachersAvgAge = "",
    StudentsAvgAge = "",
};

如果我们将计算教师平均年龄的公式交给用户定义,那么用户可能会定义一个字符串来表示:

Teachers.Sum(Age)/Teachers.Count

或者可以通过lambda来表示:

teachers.Average(teacher => teacher.Age)

此时我们就获得了字符串类型的表达式,如何进行解析呢?

二、构建字符串表达式

手动构造

这种方式是使用 Expression 类手动构建表达式,虽然不符合我们的实际需求,但是它是Dynamic.Core底层实现的方式。Expression 类的文档地址为::
https://learn.microsoft.com/zh-cn/dotnet/api/system.linq.expressions.expression?view=net-6.0

// 创建参数表达式
var teachersParam = Expression.Parameter(typeof(Teacher[]), "teachers");

// 创建变量表达式
var teacherVar = Expression.Variable(typeof(Teacher), "teacher");

// 创建 lambda 表达式
var lambdaExpr = Expression.Lambda<Func<Teacher[], double>>(
    Expression.Block(
        new[] { teacherVar }, // 定义变量
        Expression.Call(
            typeof(Enumerable),
            "Average",
            new[] { typeof(Teacher) },
            teachersParam,
            Expression.Lambda(
                Expression.Property(
                    teacherVar, // 使用变量
                    nameof(Teacher.Age)
                ),
                teacherVar // 使用变量
            )
        )
    ),
    teachersParam
);

// 编译表达式树为委托
var func = lambdaExpr.Compile();

var avgAge = func(teachers);

使用System.Linq.Dynamic.Core

System.Linq.Dynamic.Core 是一个开源库,它提供了在运行时构建和解析 Lambda 表达式树的功能。它的原理是使用 C# 语言本身的语法和类型系统来表示表达式,并通过解析和编译代码字符串来生成表达式树。

// 构造 lambda 表达式的字符串形式
string exprString = "teachers.Average(teacher => teacher.Age)";

// 解析 lambda 表达式字符串,生成表达式树
var parameter = Expression.Parameter(typeof(Teacher[]), "teachers");
var lambdaExpr = DynamicExpressionParser.ParseLambda(new[] { parameter }, typeof(double), exprString);

// 编译表达式树为委托
var func = (Func<Teacher[], double>)lambdaExpr.Compile();

// 计算教师平均年龄
var avgAge = func(teachers);

三、介绍System.Linq.Dynamic.Core

使用此动态 LINQ 库,我们可以执行以下操作:

  • 通过 LINQ 提供程序进行的基于字符串的动态查询。
  • 动态分析字符串以生成表达式树,例如ParseLambda和Parse方法。
  • 使用CreateType方法动态创建数据类。

功能介绍

普通的功能此处不赘述,如果感兴趣,可以从下文提供文档地址去寻找使用案例。

  1. 添加自定义方法类

可以通过在静态帮助程序/实用工具类中定义一些其他逻辑来扩展动态 LINQ 的分析功能。为了能够做到这一点,有几个要求:

  • 该类必须是公共静态类
  • 此类中的方法也需要是公共的和静态的
  • 类本身需要使用属性进行注释[DynamicLinqType]
[DynamicLinqType]
public static class Utils
{
    public static int ParseAsInt(string value)
    {
        if (value == null)
        {
             return 0;
        }

        return int.Parse(value);
    }

    public static int IncrementMe(this int values)
    {
        return values + 1;
    }
}

此类有两个简单的方法:

当输入字符串为 null 时返回整数值 0,否则将字符串解析为整数
使用扩展方法递增整数值

用法:

var query = new [] { new { Value = (string) null }, new { Value = "100" } }.AsQueryable();
var result = query.Select("Utils.ParseAsInt(Value)");

除了以上添加[DynamicLinqType]属性这样的方法,我们还可以在配置中添加。

public class MyCustomTypeProvider : DefaultDynamicLinqCustomTypeProvider
{
    public override HashSet<Type> GetCustomTypes() =>
        new[] { typeof(Utils)}.ToHashSet();
}

文档地址

使用项目

四、浅析System.Linq.Dynamic.Core

System.Linq.Dynamic.Core中 DynamicExpressionParser 和 ExpressionParser 都是用于解析字符串表达式并生成 Lambda 表达式树的类,但它们之间有一些不同之处。

ExpressionParser 类支持解析任何合法的 C# 表达式,并生成对应的表达式树。这意味着您可以在表达式中使用各种运算符、方法调用、属性访问等特性。

DynamicExpressionParser 类则更加灵活和通用。它支持解析任何语言的表达式,包括动态语言和自定义 DSL(领域特定语言)

我们先看ExpressionParser这个类,它用于解析字符串表达式并生成 Lambda 表达式树。

我只抽取重要的和自己感兴趣的属性和方法。

public class ExpressionParser
{
    //字符串解析器的配置,比如区分大小写、是否自动解析类型、自定义类型解析器等
    private readonly ParsingConfig _parsingConfig;

    //查找指定类型中的方法信息,通过反射获取MethodInfo
    private readonly MethodFinder _methodFinder;

    //用于帮助解析器识别关键字、操作符和常量值
    private readonly IKeywordsHelper _keywordsHelper;

    //解析字符串表达式中的文本,用于从字符串中读取字符、单词、数字等
    private readonly TextParser _textParser;

    //解析字符串表达式中的数字,用于将字符串转换为各种数字类型
    private readonly NumberParser _numberParser;

    //用于帮助生成和操作表达式树
    private readonly IExpressionHelper _expressionHelper;

    //用于查找指定名称的类型信息
    private readonly ITypeFinder _typeFinder;

    //用于创建类型转换器
    private readonly ITypeConverterFactory _typeConverterFactory;

    //用于存储解析器内部使用的变量和选项。这些变量和选项不应该由外部代码访问或修改
    private readonly Dictionary<string, object> _internals = new();

    //用于存储字符串表达式中使用的符号和值。例如,如果表达式包含 @0 占位符,则可以使用 _symbols["@0"] 访问其值。
    private readonly Dictionary<string, object?> _symbols;

    //表示外部传入的参数和变量。如果表达式需要引用外部的参数或变量,则应该将它们添加到 _externals 中。
    private IDictionary<string, object>? _externals;

    /// <summary>
    /// 使用TextParser将字符串解析为指定的结果类型.
    /// </summary>
    /// <param name="resultType"></param>
    /// <param name="createParameterCtor">是否创建带有相同名称的构造函数</param>
    /// <returns>Expression</returns>
    public Expression Parse(Type? resultType, bool createParameterCtor = true)
    {
        _resultType = resultType;
        _createParameterCtor = createParameterCtor;

        int exprPos = _textParser.CurrentToken.Pos;
        //解析条件运算符表达式
        Expression? expr = ParseConditionalOperator();
        //将返回的表达式提升为指定类型
        if (resultType != null)
        {
            if ((expr = _parsingConfig.ExpressionPromoter.Promote(expr, resultType, true, false)) == null)
            {
                throw ParseError(exprPos, Res.ExpressionTypeMismatch, TypeHelper.GetTypeName(resultType));
            }
        }
        //验证最后一个标记是否为 TokenId.End,否则抛出语法错误异常
        _textParser.ValidateToken(TokenId.End, Res.SyntaxError);
        
        return expr;
    }

    // ?: operator
    private Expression ParseConditionalOperator()
    {
        int errorPos = _textParser.CurrentToken.Pos;
        Expression expr = ParseNullCoalescingOperator();
        if (_textParser.CurrentToken.Id == TokenId.Question)
        {
           ......
        }
        return expr;
    }

    // ?? (null-coalescing) operator
    private Expression ParseNullCoalescingOperator()
    {
        Expression expr = ParseLambdaOperator();
        ......
        return expr;
    }
    // => operator - Added Support for projection operator
    private Expression ParseLambdaOperator()
    {
        Expression expr = ParseOrOperator();
        ......
        return expr;
    }

}

"If debugging is the process of removing software bugs, then programming must be the process of putting them in." - Edsger Dijkstra

“如果调试是消除软件Bug的过程,那么编程就是产出Bug的过程。” —— 艾兹格·迪杰斯特拉

0x00 大纲

0x01 前言

当你的应用程序同时处理多个用户的请求时,你会看到日志文件或者控制台中的输出通常都是交错的,而非线性连续的。尤其是在分布式系统中,一个用户请求可能包含了若干次的服务节点调用,它的日志也因此变得碎片化,如果缺乏一个用于归类和关联的标识,就会导致这笔交易难以被跟踪和追查。

MDC(Mapped Diagnostic Context)
是一种用于区分来自不同来源日志的工具。它的本质是一个由日志框架维护的
Map
存储结构,应用程序可以向其中写入键值对,并被日志框架访问。我们常用的日志门面
SLF4J
就对
MDC
的实现进行了抽象,由日志框架提供真正的实现。在
SLF4J
的文档中写道:

This class hides and serves as a substitute for the underlying logging system's MDC implementation.

If the underlying logging system offers MDC functionality, then SLF4J's MDC, i.e. this class, will delegate to the underlying system's MDC. Note that at this time, only two logging systems, namely log4j and logback, offer MDC functionality. For java.util.logging which does not support MDC, BasicMDCAdapter will be used. For other systems, i.e. slf4j-simple and slf4j-nop, NOPMDCAdapter will be used.

Thus, as a SLF4J user, you can take advantage of MDC in the presence of log4j, logback, or java.util.logging, but without forcing these systems as dependencies upon your users.

目前为止只有
logback

log4j(log4j2)
提供了较为完备的实现,其余日志框架下会使用
SLF4J
内部实现的
BasicMDCAdapter
或者
NOPMDCAdapter
.

0x02 应用场景

CLI 程序


logback
为例,我们创建一个简单的
logback.xml
配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="false">
    <property name="log.pattern" value="[%thread][%level][%logger{42}][%X{traceId:--}] %msg%n%ex"/>

    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>${log.pattern}</pattern>
        </layout>
    </appender>

    <root level="INFO">
        <appender-ref ref="stdout"/>
    </root>
</configuration>

一个简单的类用于测试,我们用一个循环来模拟用户两个独立的请求:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.util.UUID;

public class Main {
    private static final Logger LOGGER = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            try {
                LOGGER.info("Empty MDC Before Putting Data.");
                MDC.put("traceId", UUID.randomUUID().toString());
                LOGGER.info("Hello MDC.");
                LOGGER.info("GoodBye MDC.");
                throw new RuntimeException("Test Exception");
            } catch (RuntimeException e) {
                LOGGER.error("Test MDC", e);
            } finally {
                MDC.clear();
                LOGGER.info("Empty MDC After Clearing Data.");
            }
        }
    }
}

运行之后,我们会得到类似这样的日志输出:

[main][INFO][com.example.Main][-] Empty MDC Before Putting Data.
[main][INFO][com.example.Main][9ed7cc12-3880-4a38-94d4-b7ba96f37234] Hello MDC.
[main][INFO][com.example.Main][9ed7cc12-3880-4a38-94d4-b7ba96f37234] GoodBye MDC.
[main][ERROR][com.example.Main][9ed7cc12-3880-4a38-94d4-b7ba96f37234] Test MDC
java.lang.RuntimeException: Test Exception
	at com.example.Main.main(Main.java:19)
[main][INFO][com.example.Main][-] Empty MDC After Clearing Data.
[main][INFO][com.example.Main][-] Empty MDC Before Putting Data.
[main][INFO][com.example.Main][ab94804a-4f9a-4474-ba23-98542884d0ea] Hello MDC.
[main][INFO][com.example.Main][ab94804a-4f9a-4474-ba23-98542884d0ea] GoodBye MDC.
[main][ERROR][com.example.Main][ab94804a-4f9a-4474-ba23-98542884d0ea] Test MDC
java.lang.RuntimeException: Test Exception
	at com.example.Main.main(Main.java:19)
[main][INFO][com.example.Main][-] Empty MDC After Clearing Data.

可以看到,两次请求的
traceId
是不一样的,这样就能在日志中将它们区分和识别开来。通常来说,最好在请求完成后对
MDC
中的数据进行清理,尤其是使用了线程池的情况,由于线程是复用的,除非对原来的键值进行了覆盖,否则它将保留上一次的值。

Web 应用(服务端)


CLI
程序中,我们可以用上面的写法来设置
traceId
,当时对于 Web 应用,由于
Controller
入口众多,不可能每个控制器都这样子写,可以使用拦截器实现公共逻辑,避免对
Controller
的方法造成污染。先增加一个简单的
Controller
,它有两个请求处理方法,一个同步,一个异步:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Map;
import java.util.concurrent.Callable;

@RestController
public class MDCController {
    private static final Logger LOGGER = LoggerFactory.getLogger(MDCController.class);

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new LogInterceptor()).addPathPatterns("/**");
        }
    }

    @GetMapping("/syncMDC")
    public String mdcSync() {
        LOGGER.info("sync MDC test.");
        return "syncMDC";
    }

    @GetMapping("/asyncMDC")
    public Callable<String> mdcAsync() {
        LOGGER.info("async MDC test.");
        Map<String, String> mdcMap = MDC.getCopyOfContextMap();
        return () -> {
            try {
                MDC.setContextMap(mdcMap);
                LOGGER.info("异步业务逻辑处理");
                return "asyncMDC";
            } finally {
                MDC.clear();
            }
        };
    }
}

然后是关键的
MDC
拦截器:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import javax.servlet.DispatcherType;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

/**
 * 日志增强拦截器,给输出日志加上链路跟踪号
 *
 * @author YanFaBu
 **/
public class LogInterceptor implements AsyncHandlerInterceptor {
    private static final Logger LOGGER = LoggerFactory.getLogger(LogInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (request.getDispatcherType() != DispatcherType.REQUEST) {
            // 非 DispatcherType.REQUEST 分发类型,尝试从 Attribute 获取 LOG_TRACE_ID
            MDC.put("traceId", (String) request.getAttribute("traceId"));
            LOGGER.info("preHandle Non DispatcherType.REQUEST type with DispatcherType {}", request.getDispatcherType());
            return true;
        }
        // 如果本次调用来自上游服务,那么尝试从请求头获取上游传递的 traceId
        String traceId = request.getHeader("traceId");
        if (traceId == null) {
            // 本服务节点是起始服务节点,设置 traceId
            traceId = UUID.randomUUID().toString();
        }
        MDC.put("traceId", traceId);
        // 异步处理会在内部进行 Request 转发,通过 Attribute 携带 traceId
        request.setAttribute("traceId", traceId);
        LOGGER.info("preHandle DispatcherType.REQUEST type with DispatcherType {}", request.getDispatcherType());
        return true;
    }

    @Override
    public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 清理 MDC
        LOGGER.info("afterConcurrentHandlingStarted Clearing MDC.");
        MDC.clear();
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) {
        // 清理 MDC
        LOGGER.info("afterCompletion Clearing MDC with DispatcherType {}", request.getDispatcherType());
        MDC.clear();
    }
}

分别访问这两个
Controller
方法,应当看到类似这样的日志输出:

[http-nio-8080-exec-7][INFO][com.example.LogInterceptor][e828f77b-9c0d-42c5-83db-15f19153bf19] preHandle DispatcherType.REQUEST type with DispatcherType REQUEST
[http-nio-8080-exec-7][INFO][com.example.MDCController][e828f77b-9c0d-42c5-83db-15f19153bf19] sync MDC test.
[http-nio-8080-exec-7][INFO][com.example.LogInterceptor][e828f77b-9c0d-42c5-83db-15f19153bf19] afterCompletion Clearing MDC with DispatcherType REQUEST

[http-nio-8080-exec-8][INFO][com.example.LogInterceptor][7dc0878c-c014-44de-97d4-92108873a030] preHandle DispatcherType.REQUEST type with DispatcherType REQUEST
[http-nio-8080-exec-8][INFO][com.example.MDCController][7dc0878c-c014-44de-97d4-92108873a030] async MDC test.
[http-nio-8080-exec-8][INFO][com.example.LogInterceptor][7dc0878c-c014-44de-97d4-92108873a030] afterConcurrentHandlingStarted Clearing MDC.
[task-3][INFO][com.example.MDCController][7dc0878c-c014-44de-97d4-92108873a030] 异步业务逻辑处理
[http-nio-8080-exec-9][INFO][com.example.LogInterceptor][7dc0878c-c014-44de-97d4-92108873a030] preHandle Non DispatcherType.REQUEST type with DispatcherType ASYNC
[http-nio-8080-exec-9][INFO][com.example.LogInterceptor][7dc0878c-c014-44de-97d4-92108873a030] afterCompletion Clearing MDC with DispatcherType ASYNC

注意到异步请求处理中的线程号的变化,请求受理-业务处理-请求应答历经了3个不同的线程,凡是跨线程的处理逻辑,必然需要对MDC的传递进行处理,否则链路跟踪会丢失。网上看到过很多例子,都忽略了对
DispatcherType
的处理,这样就会导致异步请求中,有一部分日志会失去追踪,导致最终排查问题时链路不完整。通过
Attribute
传递不是唯一的方式,也可以借助其他上下文来传递。

Web 应用(客户端)

OkHttp 同步请求

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import java.io.IOException;
import java.util.Objects;
import java.util.UUID;

public class Client {
    private static final Logger LOGGER = LoggerFactory.getLogger(LogInterceptor.class);

    public static void main(String[] args) throws IOException {
        okHttpSync();
    }

    public static void okHttpSync() throws IOException {
        try {
            String traceId = UUID.randomUUID().toString();
            MDC.put("traceId", traceId);
            LOGGER.info("okHttpSync request syncMDC");
            OkHttpClient client = new OkHttpClient().newBuilder()
                    .build();
            Request request = new Request.Builder()
                    .url("http://localhost:8080/syncMDC")
                    .method("GET", null)
                    .addHeader("traceId", traceId)
                    .build();
            try (Response response = client.newCall(request).execute()) {
                LOGGER.info("okHttpSync response:{}", Objects.requireNonNull(response.body()).string());
            }
        } finally {
            MDC.clear();
        }
    }
}

OkHttp 异步请求

import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;

public class Client {
    private static final Logger LOGGER = LoggerFactory.getLogger(LogInterceptor.class);

    public static void main(String[] args) {
        okHttpAsync();
    }

    public static void okHttpAsync() {
        try {
            String traceId = UUID.randomUUID().toString();
            MDC.put("traceId", traceId);
            LOGGER.info("okHttpAsync request syncMDC");
            OkHttpClient client = new OkHttpClient().newBuilder()
                    .build();
            Request request = new Request.Builder()
                    .url("http://localhost:8080/syncMDC")
                    .method("GET", null)
                    .addHeader("traceId", traceId)
                    .build();
            Map<String, String> mdcMap = MDC.getCopyOfContextMap();
            client.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                    try {
                        MDC.setContextMap(mdcMap);
                        LOGGER.error("okHttpAsync error", e);
                    } finally {
                        MDC.clear();
                    }
                }

                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    try {
                        MDC.setContextMap(mdcMap);
                        LOGGER.info("okHttpAsync response:{}", Objects.requireNonNull(response.body()).string());
                    } finally {
                        MDC.clear();
                    }
                }
            });
        } finally {
            MDC.clear();
        }
    }
}

顺利的话,在客户端应该会得到类似下面的日志输出(注意线程名称的变化):

[main][INFO][com.example.Client][53924455-0fcd-442b-a5aa-aaa33005d299] okHttpSync request syncMDC
[main][INFO][com.example.Client][53924455-0fcd-442b-a5aa-aaa33005d299] okHttpSync response:syncMDC

[main][INFO][com.example.Client][5cb52293-c8ac-4bc5-87fc-dbeb1e727eba] okHttpAsync request syncMDC
[OkHttp http://localhost:8080/...][INFO][com.example.Client][5cb52293-c8ac-4bc5-87fc-dbeb1e727eba] okHttpAsync response:syncMDC

在服务端对应的日志如下,可以看到
traceId
是一致的(如果不一致或者没有看到
traceId
,应该检查下上一章提到的拦截器是否有被正确实现):

[http-nio-8080-exec-2][INFO][com.example.LogInterceptor][53924455-0fcd-442b-a5aa-aaa33005d299] preHandle DispatcherType.REQUEST type with DispatcherType REQUEST
[http-nio-8080-exec-2][INFO][com.example.MDCController][53924455-0fcd-442b-a5aa-aaa33005d299] sync MDC test.
[http-nio-8080-exec-2][INFO][com.example.LogInterceptor][53924455-0fcd-442b-a5aa-aaa33005d299] afterCompletion Clearing MDC with DispatcherType REQUEST

[http-nio-8080-exec-3][INFO][com.example.LogInterceptor][5cb52293-c8ac-4bc5-87fc-dbeb1e727eba] preHandle DispatcherType.REQUEST type with DispatcherType REQUEST
[http-nio-8080-exec-3][INFO][com.example.MDCController][5cb52293-c8ac-4bc5-87fc-dbeb1e727eba] sync MDC test.
[http-nio-8080-exec-3][INFO][com.example.LogInterceptor][5cb52293-c8ac-4bc5-87fc-dbeb1e727eba] afterCompletion Clearing MDC with DispatcherType REQUEST

处理思路都是通过
HTTP Header
携带
traceId
到下游服务,让下游服务可以跟踪来源。注意异步请求时,请求处理和应答处理回调线程不在同一个线程,需要对
MDC
的传递进行处理,否则链路跟踪会丢失。其他的客户端,如
HttpClient

Unirest
等 HTTP 请求库原理与之相似,这里就不一一列举了。

Spring WebClient


OkHttp
异步调用类似,注意要在
Mono
或者
Flux

subscribe
方法中传递
MDC
上下文。其实
WebClient
中有
Context
传递的概念,但是这块资料比较少,异步非阻塞的代码又看得头痛,暂时不想去研究了。下面的代码出于演示目的使用,请勿直接使用:

import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.web.reactive.function.client.WebClient;

import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;

public class Client {
    private static final Logger LOGGER = LoggerFactory.getLogger(LogInterceptor.class);

    public static void main(String[] args) throws InterruptedException {
        webClient();
    }

    public static void webClient() throws InterruptedException {
        String traceId = UUID.randomUUID().toString();
        MDC.put("traceId", traceId);
        LOGGER.info("webClient request syncMDC");
        WebClient client = WebClient.create("http://localhost:8080/syncMDC");
        Map<String, String> mdcMap = MDC.getCopyOfContextMap();
        CountDownLatch latch = new CountDownLatch(1);
        client.get()
                .uri("/")
                .retrieve()
                .bodyToMono(String.class)
                .subscribe(result -> {
                    try {
                        MDC.setContextMap(mdcMap);
                        LOGGER.info("webClient response:{}", result);
                    } finally {
                        MDC.clear();
                        latch.countDown();
                    }
                }, throwable -> {
                    try {
                        MDC.setContextMap(mdcMap);
                        LOGGER.error("webClient error", throwable);
                    } finally {
                        MDC.clear();
                    }
                });
        latch.await();
    }
}

输出日志如下,注意线程的变化:

[main][INFO][com.example.Client][8c984fa8-e3cd-4914-875e-ba333d31c7a9] webClient request syncMDC
[reactor-http-nio-2][INFO][com.example.Client][8c984fa8-e3cd-4914-875e-ba333d31c7a9] webClient response:syncMDC

Dubbo 服务

与 HTTP 调用类似,基于
Dubbo
的 RPC 调用也是可以跟踪的,利用
Dubbo

Filter

SPI
注册机制,我们可以增加自己的过滤器实现日志链路跟踪:

import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
import org.slf4j.MDC;
import java.util.UUID;

/**
 * 服务链路跟踪过滤器
 */
@Activate
public class RpcTraceFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        RpcContext context = RpcContext.getContext();
        boolean shouldRemove = false;
        if (context.isProviderSide()) {
            // 获取消费端设置的参数
            String traceId = context.getAttachment("traceId");
            if (traceId == null || traceId.isEmpty()) {
                traceId = MDC.get("traceId");
                if (traceId == null || traceId.isEmpty()) {
                    traceId = UUID.randomUUID().toString();
                    shouldRemove = true;
                }
            }
            // 设置 traceId
            MDC.put("traceId", traceId);
            // 继续设置下游参数,供在提供方里面作为消费端时,其他服务提供方使用这些参数
            context.setAttachment("traceId", traceId);
        } else if (context.isConsumerSide()) {
            // 如果连续调用多个服务,则会使用同个线程里之前设置的traceId
            String traceId = MDC.get("traceId");
            if (traceId == null || traceId.isEmpty()) {
                traceId = UUID.randomUUID().toString();
                // 设置 traceId
                MDC.put("traceId", traceId);
                shouldRemove = true;
            }
            // 设置传递到提供端的参数
            context.setAttachment("traceId", traceId);
        }
        try {
            return invoker.invoke(invocation);
        } finally {
            // 调用完成后移除MDC属性
            if (shouldRemove) {
                MDC.clear();
            }
        }
    }
}

在需要用到的服务模块的
resource/META-INF/dubbo/org.apache.dubbo.rpc.Filter
文件中注册过滤器(注意路径和名称不能错):

rpcTraceFilter=com.example.RpcTraceFilter

SpringBoot

application.properties
中增加配置(为了简单验证,这里没有使用注册中心。如果你想更严谨地测试,建议在本地启动两个独立的工程,并使用
ZooKeeper
进行服务注册):

dubbo.application.name=MDCExample
dubbo.scan.base-packages=com.example
dubbo.registry.address=N/A
# dubbo filter
dubbo.consumer.filter=rpcTraceFilter
dubbo.provider.filter=rpcTraceFilter

增加一个简单的
Dubbo
服务:

import org.apache.dubbo.config.annotation.DubboService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@DubboService
public class RpcService implements IRpcService {
    private static final Logger LOGGER = LoggerFactory.getLogger(RpcService.class);

    public String mdcRpc() {
        LOGGER.info("Calling RPC service.");
        return "mdcRpc";
    }
}


Controller
中增加一个方法,进行验证:

import org.apache.dubbo.config.annotation.DubboReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.Map;
import java.util.concurrent.Callable;

@RestController
public class MDCController {
    // ......(省略前面的代码)
    @DubboReference
    private IRpcService rpcService;

    @GetMapping("/mdcRpc")
    public String mdcRpc() {
        LOGGER.info("rpc MDC test.");
        return rpcService.mdcRpc();
    }
}

访问
Controller
方法,应该能得到类似下面的输出:

[http-nio-8080-exec-1][INFO][com.example.LogInterceptor][f003f750-2044-41ae-a041-8a76eb0c415b] preHandle DispatcherType.REQUEST type with DispatcherType REQUEST
[http-nio-8080-exec-1][INFO][com.example.MDCController][f003f750-2044-41ae-a041-8a76eb0c415b] rpc MDC test.
[http-nio-8080-exec-1][INFO][com.example.RpcService][f003f750-2044-41ae-a041-8a76eb0c415b] Calling RPC service.
[http-nio-8080-exec-1][INFO][com.example.LogInterceptor][f003f750-2044-41ae-a041-8a76eb0c415b] afterCompletion Clearing MDC with DispatcherType REQUEST

线程池

前面提到过跨线程调用时,需要自己处理
MDC
上下文的传递,如果是单个线程,可以手工进行处理,但如果是线程池,似乎就不能这么干了。线程池种类繁多,处理方式也有细微差别,这里不可能全部列举,以
Spring
项目中常用的
ThreadPoolTaskExecutor
为例,我们可以利用它提供的
setTaskDecorator
方法对任务进行装饰:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.Map;
import java.util.UUID;

public class MDCExecutor {
    private static final Logger LOGGER = LoggerFactory.getLogger(MDCExecutor.class);
    public static void main(String[] args) {
        MDC.put("traceId", UUID.randomUUID().toString());
        ThreadPoolTaskExecutor executor = asyncTaskExecutor();
        executor.initialize();
        executor.submit(() -> LOGGER.info("MDC Executor"));
        executor.shutdown();
    }
    @Bean
    public static ThreadPoolTaskExecutor asyncTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setTaskDecorator(task -> {
            Map<String, String> mdcMap = MDC.getCopyOfContextMap();
            return () -> {
                try {
                    if (mdcMap != null) {
                        MDC.setContextMap(mdcMap);
                    }
                    task.run();
                } finally {
                    MDC.clear();
                }
            };
        });
        return executor;
    }
}

对于其他线程池,通用的思路是覆写其
submit
或者
execute
方法来实现
MDC
传递,比如我们下面提到的定时任务调度线程池。

定时任务

除了
Controller
和 RPC 接口发起的调用,最常见的就是定时任务了。如果是定时任务作为业务发起源,可以在任务调度的时候对
MDC
进行处理。这块处理比较复杂,暂时没有找到比较优雅的切入点:

增加一个实现
RunnableScheduledFuture
接口的
DecoratedFuture
类:

import org.slf4j.MDC;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.*;

class DecoratedFuture<V> implements RunnableScheduledFuture<V> {
    Runnable runnable;
    Callable<V> callable;
    final RunnableScheduledFuture<V> task;

    public DecoratedFuture(Runnable r, RunnableScheduledFuture<V> task) {
        this.task = task;
        runnable = r;
    }

    public DecoratedFuture(Callable<V> c, RunnableScheduledFuture<V> task) {
        this.task = task;
        callable = c;
    }

    @Override
    public boolean isPeriodic() {
        return task.isPeriodic();
    }

    @Override
    public void run() {
        try {
            Map<String, String> mdcMap = MDC.getCopyOfContextMap();
            Optional.ofNullable(mdcMap).ifPresent(MDC::setContextMap);
            String traceId = MDC.get("traceId");
            if (traceId == null || traceId.isEmpty()) {
                traceId = UUID.randomUUID().toString();
            }
            MDC.put("traceId", traceId);
            task.run();
        } finally {
            MDC.clear();
        }
    }

    @Override
    public boolean cancel(boolean mayInterruptIfRunning) {
        return task.cancel(mayInterruptIfRunning);
    }

    @Override
    public boolean isCancelled() {
        return task.isCancelled();
    }

    @Override
    public boolean isDone() {
        return task.isDone();
    }

    @Override
    public V get() throws InterruptedException, ExecutionException {
        return task.get();
    }

    @Override
    public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
        return task.get(timeout, unit);
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return task.getDelay(unit);
    }

    @Override
    public int compareTo(Delayed o) {
        return task.compareTo(o);
    }

    @Override
    public int hashCode() {
        return task.hashCode();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        DecoratedFuture<?> that = (DecoratedFuture<?>) o;
        return this.task.equals(that.task);
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public RunnableScheduledFuture<V> getTask() {
        return task;
    }

    public Callable<V> getCallable() {
        return callable;
    }
}

增加一个实现
ThreadPoolTaskScheduler
接口的
DecoratedThreadPoolTaskScheduler
类:

import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;

class DecoratedThreadPoolTaskScheduler extends ThreadPoolTaskScheduler {
    private static final long serialVersionUID = 1L;

    static Runnable withTraceId(Runnable task) {
        Map<String,String> mdcMap = MDC.getCopyOfContextMap();
        return ()-> {
            try {
                Optional.ofNullable(mdcMap).ifPresent(MDC::setContextMap);
                task.run();
            } finally {
                MDC.clear();
            }
        };
    }

    @Override
    protected ScheduledExecutorService createExecutor(int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {
        return new DecoratedScheduledThreadPoolExecutor(poolSize, threadFactory, rejectedExecutionHandler);
    }
    
    @Override
    public ScheduledFuture<?> schedule(Runnable task, Instant startTime) {
        return super.schedule(withTraceId(task), startTime);
    }
    
    @Override
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) {
        return super.scheduleAtFixedRate(withTraceId(task), startTime, period);
    }
    
    @Override
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Duration period) {
        return super.scheduleAtFixedRate(withTraceId(task), period);
    }
    
    @Override
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) {
        return super.scheduleWithFixedDelay(withTraceId(task), startTime, delay);
    }
    
    @Override
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Duration delay) {
        return super.scheduleWithFixedDelay(withTraceId(task), delay);
    }
}

增加一个继承
ScheduledThreadPoolExecutor
类的
DecoratedScheduledThreadPoolExecutor
类,覆写它的两个
decorateTask
方法:

import java.util.concurrent.*;

class DecoratedScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor {
    public DecoratedScheduledThreadPoolExecutor(int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {
        super(poolSize, threadFactory, rejectedExecutionHandler);
    }

    @Override
    protected <V> RunnableScheduledFuture<V> decorateTask(Runnable runnable, RunnableScheduledFuture<V> task) {
        return new DecoratedFuture<>(runnable, task);
    }

    @Override
    protected <V> RunnableScheduledFuture<V> decorateTask(Callable<V> callable, RunnableScheduledFuture<V> task) {
        return new DecoratedFuture<>(callable, task);
    }
}

在定时任务
Configuration
中,创建
DecoratedThreadPoolTaskScheduler
作为调度线程池:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

/**
 * 定时调度配置
 */
@Configuration
@EnableAsync
@EnableScheduling
public class SchedulingConfiguration implements SchedulingConfigurer {
    public static final String TASK_SCHEDULER = "taskScheduler";

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setTaskScheduler(taskScheduler());
    }

    @Bean(TASK_SCHEDULER)
    public ThreadPoolTaskScheduler taskScheduler() {
        return new DecoratedThreadPoolTaskScheduler();
    }
}

添加一个简单定时任务:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.Scheduled;

@SpringBootApplication
public class App {
    private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @Scheduled(fixedDelay = 1500)
    public void cronTaskConfigRefresh() {
        LOGGER.info("MDC task scheduler.");
    }
}

可以看到类似下面的输出,说明就成功了:

[taskScheduler-1][INFO][com.example.App][0959d1a6-4680-4a95-a29b-b62694f0d348] MDC task scheduler.
[taskScheduler-1][INFO][com.example.App][8f034b1e-db40-44cb-9fc2-986eb8f0da6d] MDC task scheduler.
[taskScheduler-1][INFO][com.example.App][02428e88-53f8-4151-aba0-86e069c96462] MDC task scheduler.
[taskScheduler-1][INFO][com.example.App][fcd5d925-95e0-4e28-aa68-39e765668dde] MDC task scheduler.
[taskScheduler-1][INFO][com.example.App][b8ed50c6-0d6d-40c0-b170-976717fe7d22] MDC task scheduler.
[taskScheduler-1][INFO][com.example.App][9d173a26-41d4-43dc-beae-731a9f267288] MDC task scheduler.
[taskScheduler-1][INFO][com.example.App][0257c93a-9bec-40b7-9447-5a938bd2ce5f] MDC task scheduler.

0x03 小结

在实际项目中,通过灵活组合上面的若干种手段,就可以实现轻量化的日志链路跟踪,在大部分情况下基本上已经够用了,当然你也可以引入
SkyWalking

ZipKin
等探针框架,它们提供的功能也更全面更丰富。如何选择,需要根据具体项目自行权衡。

0x04 附录

样例工程下载

chatGPT正式发布已经有段时间了,这段时间我也深度体验了chatGPT的魅力。

OpenAI除了提供网页版的chatGPT,还通过api的形式提供了很多其它服务,包括文字纠错、图片生成、音频转换等等。

作为程序员,即使有现成的
openai库
,但还是免不了想自己造轮子,所以就有
这个openai库

当前这个库刚刚开发完成,还有很多需要优化的地方,所要实现的功能都是
OpenAI API
提供的,目前已经完成了以下接口的开发:

以下接口还未实现:

Engines
已经废弃,其功能由
Models
提供。

在项目的
cmd
目录下提供了一个简单地http服务,实现了对上面接口的调用。

以下是chatGPT的接口调用的简单示例:

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/mengbin92/openai"
)

func main() {
	client := openai.NewClient("your token", "your org", "proxy")

	resp, err := client.CreateChatCompletion(
		context.Background(),
		&openai.ChatCompletionRequset{
			Model: openai.GPT3Dot5Turbo,
			Messages: []openai.Message{
				{Role: openai.ChatMessageRoleUser, Content: "hi!"},
			},
		},
	)
	if err != nil {
		fmt.Printf("CreateChatCompletion error: %s\n", err.Error())
		os.Exit(-1)
	}
	fmt.Println(resp.Choices[0].Message.Content)
}


声明:本作品采用
署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
进行许可,使用时请注明出处。
Author:
mengbin
blog:
mengbin
Github:
mengbin92
cnblogs:
恋水无意