人手改的那一手真正落得下去;界面不撒谎那几处补齐

手改是**兜 AI 的错**的那一手 —— 这一轮把它从「摆在那儿但落不了地」做成真的:

件上就地改
· 保存**按地址收**,不看当下那张卡:载荷从前借道工单的 `blocks[i]` 取列名,
  于是熔断卡(没有 blocks)在屏上时,人补的那一格一个字都送不出去 ——
  而他要补的恰恰是把这一单卡住的那个必填格。授权面改由活动清单的台账声明把关。
· **能增删行**:错不止「这一格填错了」,还有「这一行不该在」与「漏了一行」。
  删挂记号不当场抹掉(看得见、可后悔),且**删要说出来**(`drops`)——
  「没回传就是删了」会把一次不完整的回传变成一次删表。
· **必填没填按不下提交**:与放行门 L1 同一个数同一套算法;闸上摆出差在哪一行哪一格。
· 只写真改了的那几行 —— 改一格却留「修订 4 行」的痕,那句话是假的。

判据与门
· 裁量判据带谓词时**谓词说了算**:「选填缺口已补齐」基座数得出来,
  人填好的目标价它照样判「客户未提供」的毛病由此消掉。
· 验收卡判据**读时现算** —— 人在成果件上补一格,判据跟着变。
· 放行门认「**办完了没有**」不认「有人写过」:从前写个 `false` 就把门推开,
  于是一单没打标就进了放行门。三个数全由基座现数。
· 拒回把「活没干完」说成「数或行对不上」的毛病:判据点名的表一行都没写时直说那一步没办。
  **指向两扇打不开的门,比不指还糟。**

界面不撒谎
· 日志只有三色:负责人绿 · 智能体蓝 · 系统灰(归侧五种是度量的分法,不是给人看的)。
· 验收收口答两件事:落了什么(按**交付物**点名,一份成果的几张表并成一行)、
  往哪儿去(转出哪一口、带过去哪些字段);一个口都没发就直说到此为止。
· 签完到收尾之间那一段有自己的话「字已签下,正在落地」—— 中间态本来就该说出来,只是要说对。
· 消息不再被内部事件刷屏:只有从通道进来的件才谈得上「无人认领」。
· 建客户公司与联系人**一次确认落两行**;联系人编码自增不问人;卡面摆表的人读名。

A1 三案例端到端全 released:全 A 类直通 12 拍 / 客户点名跳一条 10 拍 / 新客户缺必填 14 拍。
五道门禁全绿(971 passed)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsTDjoHovERXyy6Wmw1ALp
This commit is contained in:
2026-08-02 13:41:44 +08:00
co-authored by Claude Opus 5
parent 94f1c7a4ef
commit 259192f736
228 changed files with 27850 additions and 2847 deletions
+8
View File
@@ -22,6 +22,14 @@
被依赖: toolbox, memory, loop-engine, flow-engine, skill-engine, task, activity-registry,
report(叙述性小结)、app(治理台 · 系统管理路由)
近变(2026-07-30):`ModelCall.reasoning?: 'off' | 'low'` —— **压推理**。
混合推理模型的隐藏思考按秒计:实测 A1 一拍可见出参 1538 字、推理正文 9808 字,
48.6s 里八成五在解码没人读的字;关掉后同一单每拍 1.5-4.9s(十倍差)。
厂商姿势收在 provider-profile 的 `reasoningControl`(DeepSeek:off →
`thinking:{type:'disabled'}`,low → `reasoning_effort:'low'` —— 探针核过;
low 在该厂身上实测无效,推理长度不降);没声明姿势的厂家如实忽略。
回放指纹已带上 reasoning(关思考录的带不被开思考的重放命中)。
不变量:
- **一切模型调用经 `ModelGateway`** —— 绕过它,回放审就对它失明(§12.5#10)
- `replay` 模式**绝不回退实调**:缺录即 `ReplayMissError`
@@ -26,3 +26,19 @@ describe('Provider 画像', () => {
expect(resolveBaseUrl('openai-compatible', undefined)).toBe('');
});
});
describe('压推理的厂商姿势', () => {
/*
* 混合推理模型的隐藏思考按秒计:实测一拍可见出参 1538 字、推理 9808 字,
* 48.6s 里八成五在解码没人读的思考。姿势收在画像里 —— 新厂家只改画像,不加 if。
*/
it('DeepSeek:off 关思考,low 降档 —— 参数名探针核过', () => {
const p = profileOf('deepseek');
expect(p.reasoningControl?.off).toEqual({ thinking: { type: 'disabled' } });
expect(p.reasoningControl?.low).toEqual({ reasoning_effort: 'low' });
});
it('没声明姿势的厂家:调用方的意愿如实忽略,不假装生效', () => {
expect(profileOf('openai').reasoningControl).toBeUndefined();
});
});
+6
View File
@@ -45,6 +45,12 @@ export type ModelCall = {
readonly prompt: string;
readonly temperature?: number;
readonly maxOutputTokens?: number;
/**
* 压推理:`off` 不许想、`low` 少想;缺省由着模型。
* 只对画像里声明了 `reasoningControl` 的厂家生效 —— 没姿势的厂家如实忽略。
* 规划型调用(loop 单拍)该关:隐藏思考按秒计,而它的判断本就交给了判定表与工具。
*/
readonly reasoning?: 'off' | 'low';
/** 出参 schema(JSON Schema);给定即要求结构化出参并校验 */
readonly outputSchema?: Record<string, unknown>;
/** 录制/回放的定位:任务 × 步序号(§7.3) */
@@ -33,6 +33,17 @@ export function createProviderInvoker(): ProviderInvoker {
.filter((line): line is string => line !== undefined && line !== '')
.join('\n\n');
/*
* providerOptions 是**合并体**:JSON 模式与压推理都往请求体里加参,得进同一个对象 ——
* 两处各自 spread 会互相覆盖(踩不得)。
*/
// AI SDK 的 providerOptions 值型是 JSONObject —— 从 generateText 的形参上取,不额外引包
type ProviderBody = NonNullable<Parameters<typeof generateText>[0]['providerOptions']>[string];
const bodyExtra: ProviderBody = {};
if (useJsonMode) bodyExtra['response_format'] = { type: 'json_object' };
const knob = call.reasoning === undefined ? undefined : profile.reasoningControl?.[call.reasoning];
if (knob !== undefined) Object.assign(bodyExtra, knob as ProviderBody);
const started = Date.now();
const result = await generateText({
model: provider(binding.model),
@@ -40,7 +51,7 @@ export function createProviderInvoker(): ProviderInvoker {
prompt: call.prompt,
...(call.temperature === undefined ? {} : { temperature: call.temperature }),
...(call.maxOutputTokens === undefined ? {} : { maxOutputTokens: call.maxOutputTokens }),
...(useJsonMode ? { providerOptions: { [binding.provider]: { response_format: { type: 'json_object' } } } } : {}),
...(Object.keys(bodyExtra).length === 0 ? {} : { providerOptions: { [binding.provider]: bodyExtra } }),
});
/*
@@ -84,15 +95,51 @@ export function parseJsonLoose(text: string): unknown {
try {
return JSON.parse(candidate);
} catch {
const start = candidate.search(/[[{]/);
const end = Math.max(candidate.lastIndexOf('}'), candidate.lastIndexOf(']'));
if (start >= 0 && end > start) {
try {
return JSON.parse(candidate.slice(start, end + 1));
} catch {
return undefined;
}
}
return undefined;
return firstBalanced(candidate);
}
}
/**
* 从文本里取**第一个配平的** JSON 对象 / 数组。
*
* 从前这里取的是「第一个 `{` 到**最后一个** `}`」。模型一次吐两个对象时
* (它把这一拍和下一拍一起写了出来,中间空一行),那一刀切出来的是
* `{…}\n\n{…}` —— 非法 JSON,整段作废,于是基座判「模型未给出合规的行动」,
* 发一张兜底卡把人叫来。实测栽过:模型给的 9 颗料判据完全正确、
* 分类器都要调了,只因为它多写了一个对象,整拍白跑、人被无谓地打扰了一次。
*
* 现在按括号配平扫第一个完整对象,后面多出来的当噪声丢掉 ——
* 多写的那一份本就是下一拍的事,这一拍不该替它执行。
* 扫的时候要认字符串与转义,否则值里带 `}` 的会把括号计数带偏。
*/
function firstBalanced(text: string): unknown {
const start = text.search(/[[{]/);
if (start < 0) return undefined;
const open = text[start];
const close = open === '{' ? '}' : ']';
let depth = 0;
let inString = false;
let escaped = false;
for (let i = start; i < text.length; i += 1) {
const ch = text[i];
if (inString) {
if (escaped) escaped = false;
else if (ch === '\\') escaped = true;
else if (ch === '"') inString = false;
continue;
}
if (ch === '"') inString = true;
else if (ch === open) depth += 1;
else if (ch === close) {
depth -= 1;
if (depth === 0) {
try {
return JSON.parse(text.slice(start, i + 1));
} catch {
return undefined;
}
}
}
}
return undefined;
}
@@ -19,6 +19,15 @@ export type ProviderProfile = {
readonly structuredOutput: 'json-object' | 'prompt-only';
/** 推理模型:回包里带 reasoning,答案在正文,推理另置(不进出参、只作留痕) */
readonly emitsReasoning: boolean;
/**
* 「压推理」的厂商姿势 —— 调用方说 `reasoning: 'off' | 'low'`,画像译成请求体参数。
*
* 为什么值得一个画像字段:混合推理模型的隐藏思考按 token 计费、按解码计秒 ——
* 实测 A1 一拍可见出参 1538 字、推理正文 9808 字,48.6 秒里八成五在解码没人读的思考;
* 探针实测 DeepSeek `thinking:{type:'disabled'}` 后同一问题 89→9 出参 token、时延减半。
* 没有姿势的厂家(undefined)只能由着它想 —— 调用方的意愿如实忽略,不假装生效。
*/
readonly reasoningControl?: Readonly<Record<'off' | 'low', Record<string, unknown>>>;
readonly label: string;
};
@@ -43,6 +52,11 @@ export const PROVIDER_PROFILES: Readonly<Record<LlmProvider, ProviderProfile>> =
defaultBaseUrl: 'https://api.deepseek.com',
structuredOutput: 'json-object',
emitsReasoning: true,
// 探针核过(2026-07-30):disabled 归零推理;effort 只认 low/medium/high/xhigh/max
reasoningControl: {
off: { thinking: { type: 'disabled' } },
low: { reasoning_effort: 'low' },
},
label: 'DeepSeek',
},
anthropic: {
@@ -24,7 +24,18 @@ export function createLlmService(deps: LlmServiceDeps): LlmService {
},
async upsert(input: UpsertLlmInstance): Promise<LlmInstanceView> {
const existing = input.id === undefined ? undefined : await deps.instances.findById(input.id);
/*
* 找「原来那条」要**按 id 也按名字**。
*
* 落库是 `onConflictDoUpdate(target: name)` —— 名字才是那张表的身份。
* 从前只按 id 查:播种走的是无 id 的按名 upsert,于是 `existing` 恒为 undefined、
* `apiKeyCipher` 解析成 undefined、落库写成 null ——
* **一次不带密钥的播种就把治理台里存好的密钥擦掉了**,而界面上只会显示「未绑定」。
* 「只写不读:apiKey 留空即保持原密文」(ADR-009 #4)这句话,得在两条路上都成立。
*/
const existing =
(input.id === undefined ? undefined : await deps.instances.findById(input.id)) ??
(await deps.instances.findByName(input.name));
// 只写不读:apiKey 留空即保持原密文(ADR-009 #4)
const apiKeyCipher =
input.apiKey !== undefined && input.apiKey !== ''