Langchain.js | StringOutputParser | 疯狂输出(一)🧐

165 阅读7分钟

前言

书接上文 , 学习了 prompt , 如果我们把学习 langchain.js 看成学编程语言的过程 , prompt 就是输入的操作 , 即刚入门阶段 , 比如学 C 语言 , 一开始就是学习输入输出 , 对于"输入" prompt 已经学习了一些常用的 , 以后如果不能满足需求也可以上官网查 , 我在 "重拳出击第零式"中已经展示了 prompt 的全貌

现在学习"输出"OutputParser 同样 , 先到官网上去 , 看看 OutputParser 的全貌 , 一览众山小 ~🤡

关于以上分类我在 "重拳出击第零式"中做了相关的学习 , 这一次直接进入主题——

StringOutputParser (官网点👈)

它属于 Classes

图片取自官网 :

v03.api.js.langchain.com/modules/_la…

文章行文思路 :

  1. 探讨大模型原始输出 AIMessage

  2. StringOutparser 解析后的输出

  3. 探讨StringOutparse意义

由于StringOutparser 用法较为简单 , 我们主要研究下 AlMessage 结构细节 , 之后更好理解StringOutparser !

首先搭一个 model , 看看在没有 parser 作用下的原始输出是怎么样的 :

import {load} from "dotenv";
const env  = await load();
const process = {env}
import {ChatOpenAI} from "@langchain/openai";
import {HumanMessage} from "@langchain/core/messages";

const model = new ChatOpenAI();
const prompt = new HumanMessage("雷军: Are you ok 🤡?")
await model.invoke([
    prompt
])

我们逐行解析输出的消息 , 以便更好理解 AIMessage 的内部机制

1) lc_serializable: true

表示该对象是可序列化的 , 意味着对象可以方便地转换为一种能够存储(比如保存到文件)或者传输(比如通过网络发送)的格式(常见的像 JSON 格式等),后续可以再从这种序列化的格式还原回原始对象,便于数据的持久化和交互等操作。

2) lc_kwargs

包含了多个关键信息的对象 , 用于构造 AIMessage 这个类实例时传入的一些主要参数,类似构造函数的参数集合 , 具体来看

  • content 具体文本内容

  • additional_kwargs : 里面包含了 function_call: undefinedtool_calls: undefined,这可能意味着当前这个回复消息并没有关联特定的函数调用或者工具调用相关的需求 。在一些更复杂的语言模型应用场景中,模型可以根据输入决定是否要调用外部函数或者工具来辅助生成更完善的回复,这里显示未定义表示当前回复暂不涉及这类情况

  • response_metadata通常用于存放一些额外的关于回复的元数据信息 , 不过在这里初始时为空

3) lc_namespace

用于标识该对象所属的命名空间,在代码的模块、类库组织架构中,命名空间有助于区分不同功能模块、不同层次的对象

4) content

这里再次出现可能是为了方便直接访问这个最重要的消息内容属性,不用总是通过 lc_kwargs 去获取。

5) name

其值为 undefined,可能表示这个消息对象没有特定的名称,具体是否需要名称取决于应用场景,比如在一些多轮对话中,可能会给不同角色的消息赋予特定名称来区分,但这里没有定义相关名称。

6) additional_kwargs

重复出现了,和前面介绍的 lc_kwargs 里的 additional_kwargs 一样,再次强调了当前回复消息在函数调用和工具调用方面的情况,目前都是未定义状态。

7) response_metadata

这里有了具体的内容,包含了 tokenUsagefinish_reason 等属性:

  • tokenUsage:详细记录了令牌(token)的使用情况,其中 completionTokens15,表示生成回复内容所消耗的令牌数量;promptTokens18,意味着输入提示内容(也就是向模型提问的那部分内容)所消耗的令牌数量;totalTokens33,就是整个交互过程(包含提问和回复)总共消耗的令牌数量,令牌使用量的统计在很多按使用量计费或者分析模型性能等场景中很有用。
  • finish_reason:值为 "stop",说明模型结束生成回复的原因是正常结束,一般来说除了 "stop",可能还有其他情况比如达到了最大生成长度限制等其他终止生成的原因,这里 "stop" 表示按正常流程完成了回复的生成。

在官网中有一页 , 专门教我们追踪令牌的使用情况 , 就是结合上面的第 7 点 ,

How to track token usage

由于返回的是对象字面量 : 我们直接通过点的形式就可以获取 , 下面做做实验

改造上面代码 ;

import {load} from "dotenv";
const env  = await load();
const process = {env}
import {ChatOpenAI} from "@langchain/openai";
import {HumanMessage} from "@langchain/core/messages";

const model = new ChatOpenAI();
const prompt = new HumanMessage("雷军: Are you ok 🤡?")
const res = await model.invoke([
    prompt
])

获取tokenUsage

console.log(res.response_metadata.tokenUsage)
//{ completionTokens: 31, promptTokens: 18, totalTokens: 49 }

获取 content

console.log(res.content)
//I'm just a language model AI, so I don't have emotions or physical health,
//but thank you for asking! How can I assist you today?

对于 StringOutputParser 就是干res.content这个事情 , 对比如下 :

import {load} from "dotenv";
import {ChatOpenAI} from "@langchain/openai";
import {HumanMessage} from "@langchain/core/messages";
import {StringOutputParser} from "@langchain/core/output_parsers";
const env  = await load();
const process = {env}


const parser = new StringOutputParser();
const model = new ChatOpenAI();
const prompt = new HumanMessage("雷军: Are you ok 🤡?")


const res1 = await model.invoke([
    prompt
])
console.log(res1.content)
console.log("-----------------------")
const chain =  model.pipe(parser)
const res2 = await chain.invoke([
    prompt
])
console.log(res2)

由于我调用了两次模型 ,输出结果又差异 ,但是输出格式一致

那么我有两个疑问 :

  • 通过字面属性获取纯净的文本内容和使用 StringOutputParser 效果一致 , 那么StringOutputParser 不是多此一举吗 ?
  • StringOutputParser 底层的实现机制是不是和面属性获取纯净的文本内容一样 ?

Question 1

虽然从表面上看,直接获取 res1.content 也能拿到模型回复中的文本内容,但在更复杂的应用场景中,语言模型返回的结果格式可能是多样化的,不一定总是能方便地直接提取到想要的纯文本内容。

这里我只是使用 openai 的模型 , 如果使用其他模型 , 他们会有自己的 AIMessage 的格式 ,

在使用不同的语言模型或者构建不同的处理链路时,各个模型或链路返回的原始格式可能千差万别。

StringOutputParser 可以作为一个统一的工具,使得不管底层模型实际返回什么样的格式,经过它处理后,对于上层应用代码来说,始终能接收到统一的、标准化的字符串格式输出,增强了代码的可维护性和通用性。

比如,后续如果要替换成另一个语言模型,只要其输出能够被 StringOutputParser 适配解析,那上层依赖文本输出的业务逻辑代码基本不用做太多改动。

说到这里 ,应该知道 , StringOutputParser 怎么会多余 🤡👈

Question 2

底层实现机制

StringOutputParser 通常有其内部的一套解析逻辑,它会接收模型返回的整个对象(可能是包含各种属性的复杂结构),然后按照一定的规则和算法去提取、转换其中的文本内容部分

比如可能会判断对象中哪个属性是代表文本内容(像前面提到的 content 属性等情况),并将其提取出来,去除掉其他无关的元数据等信息,最终将其转换为一个纯粹的字符串返回。

而且它可能还会处理一些边界情况,比如对文本的编码转换、去除多余的空格或特殊字符等,以保证输出的字符串质量符合预期。

而直接访问 content 属性只是简单地从模型返回的对象结构中获取已经定义好的那个文本内容字段,并没有上述这些额外的处理、适配以及规范化的操作。

所以说,它们底层机制是不一样的,

  • StringOutputParser 是一种主动的、带有解析和规范化功能的处理方式
  • 而直接获取 content 属性是一种相对简单直接的属性访问行为

前者更侧重于对不同格式输出进行通用处理以适应更广泛的应用场景,后者则是在已知具体返回对象结构的情况下进行的最基本的文本获取操作。

综上所述,StringOutputParser 在代码中有其不可替代的作用,其底层实现机制和简单的 content 属性访问有着明显区别,共同服务于对语言模型输出进行有效处理和利用的需求。

总结

疯狂输出到这 ~ , 关于 StringOutputParser API 查阅 :v03.api.js.langchain.com/classes/_la…