详情

首页手游攻略 LangChain JS 的单次 guardrailConfig 没覆盖默认值,应该查哪一层?

LangChain JS 的单次 guardrailConfig 没覆盖默认值,应该查哪一层?

佚名 2026-11-06 16:10:01

先限定为 @langchain/aws 的 ChatBedrockConverse,再检查选项解析和构造出的 SDK 命令。本次1.5.2实测 guardrailConfig、additionalModelRequestFields 都先用构造值;不能只看回答内容就判断单次配置或护栏已经生效。

1. 三组输入:同时设置、只用默认、只用单次值

环境为 Node.js 22.23.4、@langchain/aws 1.5.2、@langchain/core 1.3.4,底层 @aws-sdk/client-bedrock-runtime 3.1162.0。安装前两个指定版本后,将公共代码保存为 fixture.mjs,再将每节代码分别保存为 01-resolve.mjs 等文件,用 Node.js 执行;每组独立运行。这里注入本地 SDK 替身,不读取真实 AWS 凭证,也不发送AWS请求。模型和护栏标识都是模拟占位,top_k 只用于区分两个 JSON 值,不代表任意模型都支持该参数。

import { ChatBedrockConverse } from '@langchain/aws';
export const defaults = {
  guardrailConfig:{guardrailIdentifier:'guardbase',guardrailVersion:'1',trace:'disabled'},
  additionalModelRequestFields:{top_k:10}
};
export const perCall = {
  guardrailConfig:{guardrailIdentifier:'guardcall',guardrailVersion:'2',trace:'enabled'},
  additionalModelRequestFields:{top_k:20}
};
// 本地SDK边界替身:捕获真实适配器构造的命令,不向AWS发送请求。
export function createProbe() {
  const seen = [];
  const client = { async send(command) {
    seen.push(structuredClone({kind:command.constructor.name,
      guardrailConfig:command.input.guardrailConfig,
      additionalModelRequestFields:command.input.additionalModelRequestFields}));
    if (command.constructor.name === 'ConverseStreamCommand') {
      return {stream:(async function* () {
        yield {messageStart:{role:'assistant'}};
        yield {contentBlockDelta:{contentBlockIndex:0,delta:{text:'ok'}}};
        yield {messageStop:{stopReason:'end_turn'}};
      })()};
    }
    return {output:{message:{role:'assistant',content:[{text:'ok'}]}},stopReason:'end_turn'};
  }};
  return {seen, client,
    model(fields = {}) { return new ChatBedrockConverse({
      model:'local-model',region:'us-east-1',client,maxRetries:0,...fields
    }); }
  };
}

先直接检查本版本的 invocationParams,它可用于本地诊断解析,不应当成跨版本稳定的业务扩展接口。构造值是 guardbase、版本1、top_k:10;单次值是 guardcall、版本2、top_k:20。下面保持相同配置,只改变值放在哪一层。

import {createProbe, defaults, perCall} from './fixture.mjs';
const probe = createProbe();
const configured = probe.model(defaults);
for (const [label, model, options] of [
  ['both', configured, perCall],
  ['fallback', configured, {}],
  ['call-only', probe.model(), perCall]
]) {
  const p = model.invocationParams(options); // 仅作本地解析诊断
  console.log(label, JSON.stringify({guardrailConfig:p.guardrailConfig,
    additionalModelRequestFields:p.additionalModelRequestFields}));
}
// 1.5.2中:both仍为guardbase/10;call-only为guardcall/20。
三组配置解析:构造值存在时优先,只有单次值时进入调用配置
三组配置解析:构造值存在时优先,只有单次值时进入调用配置;本地实测结果页,非终端或AWS控制台,未验证云端执行。

both 和 fallback 都得到 guardbase、版本1、top_k:10,call-only 才得到 guardcall、版本2、top_k:20。说明单次字段并非完全没有传进来,而是存在构造值时被它挡住;如果三组全都一样,还要检查上层封装是否丢弃了选项、字段是否放在 invoke 的第二参数顶层,以及运行时实际加载的包版本。

2. 不只看中间参数:核对 invoke 和 stream 的命令输入

继续使用相同 fixture。这次实际执行适配器的非流式和流式路径,在 client.send 入口捕获它构造出的 ConverseCommand 与 ConverseStreamCommand;替身返回的 ok 只是让流程完成,不能证明任何真实护栏策略生效。

import {createProbe, defaults, perCall} from './fixture.mjs';
const probe = createProbe();
const model = probe.model(defaults);
await model.invoke('diagnostic', perCall);
for await (const chunk of await model.stream('diagnostic', perCall)) {
  // 消费完整流,使流式路径也走到本地SDK替身。
}
console.log(JSON.stringify(probe.seen));
// 两种命令的guardrailConfig仍为guardbase,top_k仍为10。
实际适配器的非流式和流式命令都携带构造护栏配置
实际适配器的非流式和流式命令都携带构造护栏配置;本地实测结果页,非终端或AWS控制台,未验证云端执行。

两种命令都携带 guardbase、版本1、trace:disabled 与 top_k:10,覆盖问题已经在进入 SDK 传输前出现。本次验证止于命令输入,没有验证 HTTP 序列化、签名、权限、模型支持或 AWS 服务端执行;如果命令已经正确,才继续沿真实测试环境检查后面的链路。trace 是追踪配置,不能只凭启用追踪就判断覆盖顺序已修复。

3. 当前版本的临时做法:先选完整配置,再创建独立实例

在没有经过验证的修复版本、又暂不维护受控补丁时,可以在请求入口用业务规则选定整份配置,再放入本请求自己的实例。下面两次调用并发使用不同实例,共享同一个本地客户端,但不修改共享 model 的属性;真实项目也可按批准的固定策略复用独立实例,注意实例管理和缓存成本。

import {createProbe, defaults, perCall} from './fixture.mjs';
const probe = createProbe();
function requestModel(options = {}) {
  // 先决定整份配置,再放入本请求独立的模型实例。
  return probe.model({
    guardrailConfig:structuredClone(options.guardrailConfig ?? defaults.guardrailConfig),
    additionalModelRequestFields:structuredClone(
      options.additionalModelRequestFields ?? defaults.additionalModelRequestFields)
  });
}
await Promise.all([
  requestModel(perCall).invoke('override'),
  requestModel().invoke('fallback')
]);
console.log(JSON.stringify(probe.seen));
// override为guardcall/20,fallback为guardbase/10,不改共享model属性。
请求独立实例分别使用单次完整配置与默认完整配置
请求独立实例分别使用单次完整配置与默认完整配置;本地实测结果页,非终端或AWS控制台,未验证云端执行。

override 得到 guardcall、版本2、top_k:20,fallback 保留 guardbase、版本1、top_k:10。配置在进入有问题的解析层前已经选定,因此没有构造值与单次值竞争。另测了不设置这两项构造默认、每次显式传入完整已解析值的入口,也能传入单次配置;采用这种方式时,应统一封装所有调用,不能让裸调用遗漏应有默认配置。

4. ?? 的顺序决定优先级,但它不会深合并对象

本版本源码对这两个字段使用构造值在左、调用值在右的空值回退,其他部分字段则采用相反顺序。下面用最小表达式对照根因与目标顺序,不将它伪装成已经发布的补丁;本节的 null、空对象只用于解释 JavaScript 行为,不保证它们是该服务接受的调用配置。

import {defaults, perCall} from './fixture.mjs';
// 根因与目标优先级的最小对照,不是已发布版本的补丁。
const ctor = defaults.guardrailConfig;
const call = perCall.guardrailConfig;
console.log((ctor ?? call).guardrailIdentifier); // guardbase
console.log((call ?? ctor).guardrailIdentifier); // guardcall
console.log(JSON.stringify({trace:'enabled'} ?? ctor)); // 只有trace,没有自动合并id和version
console.log(JSON.stringify({} ?? ctor)); // {}:空对象仍优先
console.log((null ?? ctor).guardrailIdentifier); // guardbase
console.log((undefined ?? ctor).guardrailIdentifier); // guardbase
空值回退顺序对照及对象不自动深合并的实际输出
空值回退顺序对照及对象不自动深合并的实际输出;本地实测结果页,非终端或AWS控制台,未验证云端执行。

交换两侧后,非空的单次值才优先;但只含 trace 的对象仍只是自己的字段,并不会自动继承标识和版本,空对象也仍是一个有值的对象。null 与 undefined 在 ?? 下都会回退,因此不能当作可靠的清除或禁用开关。需要继承时应显式构造并验证完整配置,保持策略标识与版本的配对,不要随意深合并不同模型的请求 JSON。

5. 固定版本复验,再判断问题落在哪一层

截至2026年11月5日本次核查,对应修复提案仍未合并,已安装的1.5.2包也实际复现了构造值优先。不要将提交、开放PR或修改源码的建议直接写成正式版本已修复;未来更新依赖后,重新运行同一矩阵,并核对锁文件与部署产物,确认实际执行的是哪份实现。

核对项本次观察或应有判据
构造+单次都设置1.5.2中两个字段仍取构造值
没有单次配置保留明确的构造默认
没有构造配置本次单次完整配置进入解析及命令
invoke / stream两条路径都要看对应命令输入
目标顺序为单次优先仍须验证整对象替换、空值回退与并发隔离
真实服务端命令正确后再查策略标识、版本、地区、权限及既定验证用例

先对照三组输入的解析,再核对 invoke、stream 命令。临时方案在入口选定完整配置或使用独立策略实例;不要并发修改共享 model.guardrailConfig 后再恢复,也不能用本地 mock 成功证明云端护栏已运行。

点击查看更多
推荐专题
热门阅读