问数据时间线的分析一步、协作成员的分析:编号照实说是哪一层。Agent 工具那一步标题写「变化拆解(可加) · 本节点 A1」——编号是这份 Agent 自己的分析容器里的(交回模型的「【分析 A1】」),报告目录里的 A1 按整次运行的台账编,图里还有分析卡时就不是同一个,不再冒充报告里的编号;分析卡的一项照旧写这一项的标识。协作成员的分析不带编号(成员的结论还进不了台账,照它写「· A1」会和报告里点名的结论对不上),event_view 交回 alias=None。check-decode、check-stream、协作成员那条测试照新写法改

This commit is contained in:
ThearchyHelios
2026-10-21 15:52:58 +00:00
parent 9461c03a63
commit b919890b7b
7 changed files with 31 additions and 20 deletions
+3 -2
View File
@@ -462,9 +462,10 @@ async def run_supervisor(state: GraphState, ctx: NodeContext) -> dict[str, Any]:
agent=name, call_id=cid, preview=content[:1500], agent=name, call_id=cid, preview=content[:1500],
duration_ms=int((time.perf_counter() - started) * 1000), **failure, duration_ms=int((time.perf_counter() - started) * 1000), **failure,
**({"checks": checks} if checks else {}), **({"checks": checks} if checks else {}),
# 判定的那一小段同 Agent 节点(event_view);编号是成员自己的分析容器里的,紧接着 _numbered 按它编 # 判定的那一小段同 Agent 节点(event_view)。不带编号:成员自己的分析容器里的 A1 不是报告目录里的
# A1(协作成员的结论还进不了台账),时间线上照它写「· A1」会和报告里点名的结论对不上
**({"analysis_artifact": analysis["artifact"], **({"analysis_artifact": analysis["artifact"],
"analysis": event_view(analysis["result"], scope.next_alias())} if analysis else {})) "analysis": event_view(analysis["result"])} if analysis else {}))
if analysis is not None and analysis["result"].get("status") == "refused": if analysis is not None and analysis["result"].get("status") == "refused":
ctx.emit(EventType.LOG, level="warn", code=ANALYSIS_REFUSED, agent=name, call_id=cid, tool=tname, ctx.emit(EventType.LOG, level="warn", code=ANALYSIS_REFUSED, agent=name, call_id=cid, tool=tname,
reason_code=analysis["result"].get("reason_code"), reason_code=analysis["result"].get("reason_code"),
+7 -4
View File
@@ -70,7 +70,7 @@ async def test_tool_end_carries_the_phrase_and_the_same_number_the_model_got(mon
assert error["call_error"] == "unknown_column" and "analysis" not in error assert error["call_error"] == "unknown_column" and "analysis" not in error
async def test_a_team_member_numbers_its_own_analyses(monkeypatch, shop): async def test_a_team_member_analysis_carries_no_number(monkeypatch, shop):
Script(monkeypatch, [(TOOL, {"sql": SQL}), (DECOMPOSE, DECOMPOSE_ARGS)], Script(monkeypatch, [(TOOL, {"sql": SQL}), (DECOMPOSE, DECOMPOSE_ARGS)],
route={"assignments": [{"agent": "分析员", "instruction": "拆一下销售额的变化"}], "done": False}) route={"assignments": [{"agent": "分析员", "instruction": "拆一下销售额的变化"}], "done": False})
nodes = [node("start", "input"), nodes = [node("start", "input"),
@@ -82,7 +82,10 @@ async def test_a_team_member_numbers_its_own_analyses(monkeypatch, shop):
input_payload={}) input_payload={})
row = await wait(run.id) row = await wait(run.id)
assert row.status == "succeeded", row.error assert row.status == "succeeded", row.error
[end] = [e for e in await events(row.id, "tool.end", "team") if e.get("analysis_artifact")] ends = [e for e in await events(row.id, "tool.end", "team") if e.get("analysis_artifact")]
assert end["agent"] == "分析员" assert [e["agent"] for e in ends] == ["分析员"]
assert (end["analysis"]["alias"], end["analysis"]["op"], end["analysis"]["verdict"]) == ("A1", "decompose", "driver") end = ends[0]
# 不带编号:成员自己的分析容器里的 A1 不是报告目录里的 A1(协作成员的结论还进不了台账),报告目录另按整次运行的台账编
assert end["analysis"]["alias"] is None
assert (end["analysis"]["op"], end["analysis"]["verdict"]) == ("decompose", "driver")
assert json.dumps(end["analysis"], ensure_ascii=False).count("下降是不是集中在某个地区") == 0, "Agent 写的假设不带" assert json.dumps(end["analysis"], ensure_ascii=False).count("下降是不是集中在某个地区") == 0, "Agent 写的假设不带"
+5 -2
View File
@@ -222,8 +222,11 @@ export const ANALYSIS_TEXT = {
step: { step: {
/** 运行中的状态字(store/chat) */ /** 运行中的状态字(store/chat) */
running: (label: string) => `正在执行分析:${label}…`, running: (label: string) => `正在执行分析:${label}…`,
/** 一步的标题:「变化拆解(可加)· A1」;分析卡的一项:「变化拆解(可加)· where」 */ /** 分析卡的一项:「变化拆解(可加)· where」。标识是这一项在分析卡里的 id,不是报告目录里的编号 */
title: (method: string, alias?: string | null) => (alias ? `${method} · ${alias}` : method), title: (method: string, item?: string | null) => (item ? `${method} · ${item}` : method),
/** Agent 工具那一步:「变化拆解(可加)· 本节点 A1」。编号是这份 Agent 自己的分析容器里的(交回模型的「【分析 A1】」),
* 报告目录里的 A1 按整次运行的台账编,两者不一定是同一个(图里还有分析卡时就不是) */
nodeTitle: (method: string, alias?: string | null) => (alias ? `${method} · 本节点 ${alias}` : method),
callError: '调用有误,没有记成结论', callError: '调用有误,没有记成结论',
callErrorNext: '这次调用没有记成结论,Agent 会按这条说明改正参数后重新调用', callErrorNext: '这次调用没有记成结论,Agent 会按这条说明改正参数后重新调用',
refused: '没有做成', refused: '没有做成',
+2 -2
View File
@@ -739,13 +739,13 @@ export function analysisTeamRun(): RunEvent[] {
ev('tool.end', 'team', { tool: 'analysis_decompose', agent: '分析员', call_id: 'm2', duration_ms: 8, ev('tool.end', 'team', { tool: 'analysis_decompose', agent: '分析员', call_id: 'm2', duration_ms: 8,
preview: '{\n "analysis_artifact": "a1f0", \n "result": {\n "op": "decompose",\n "mode": "additive",\n "method": "decompose.additive@1",', preview: '{\n "analysis_artifact": "a1f0", \n "result": {\n "op": "decompose",\n "mode": "additive",\n "method": "decompose.additive@1",',
analysis_artifact: 'a1f0c3d2b4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f9', analysis_artifact: 'a1f0c3d2b4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f9',
analysis: { alias: 'A1', op: 'decompose', mode: 'additive', status: 'ok', verdict: 'driver', direction: 'down', phrase: TEAM_PHRASE, analysis: { alias: null, op: 'decompose', mode: 'additive', status: 'ok', verdict: 'driver', direction: 'down', phrase: TEAM_PHRASE,
reason_code: null } }, 0.01) reason_code: null } }, 0.01)
ev('tool.start', 'team', { tool: 'analysis_baseline', agent: '分析员', call_id: 'm3', args: { query: 'Q1', period: 'week', measure: 'gmv' } }, 0.1) ev('tool.start', 'team', { tool: 'analysis_baseline', agent: '分析员', call_id: 'm3', args: { query: 'Q1', period: 'week', measure: 'gmv' } }, 0.1)
ev('tool.end', 'team', { tool: 'analysis_baseline', agent: '分析员', call_id: 'm3', duration_ms: 4, ev('tool.end', 'team', { tool: 'analysis_baseline', agent: '分析员', call_id: 'm3', duration_ms: 4,
preview: '{\n "analysis_artifact": "b2e1",', preview: '{\n "analysis_artifact": "b2e1",',
analysis_artifact: 'b2e1d4c3a5f6e70819203b4c5d6e7f8091a2b3c4d5e6f70819203b4c5d6e7f80', analysis_artifact: 'b2e1d4c3a5f6e70819203b4c5d6e7f8091a2b3c4d5e6f70819203b4c5d6e7f80',
analysis: { alias: 'A2', op: 'baseline', mode: null, status: 'refused', verdict: null, direction: null, analysis: { alias: null, op: 'baseline', mode: null, status: 'refused', verdict: null, direction: null,
phrase: '波动基线没有做成:只有 0 期历史,至少要 8 期', reason_code: 'history_short' } }, 0.01) phrase: '波动基线没有做成:只有 0 期历史,至少要 8 期', reason_code: 'history_short' } }, 0.01)
ev('log', 'team', { level: 'warn', code: 'analysis_refused', agent: '分析员', call_id: 'm3', tool: 'analysis_baseline', ev('log', 'team', { level: 'warn', code: 'analysis_refused', agent: '分析员', call_id: 'm3', tool: 'analysis_baseline',
reason_code: 'history_short', reason: '只有 0 期历史,至少要 8 期', message: '分析员 的 analysis_baseline 没有做成:只有 0 期历史,至少要 8 期' }) reason_code: 'history_short', reason: '只有 0 期历史,至少要 8 期', message: '分析员 的 analysis_baseline 没有做成:只有 0 期历史,至少要 8 期' })
+6 -3
View File
@@ -1387,8 +1387,9 @@ function toolStep(seq: number, tool: string, args: Record<string, any>): Step {
} }
/** /**
* tool.end 的 analysis(tools/analysis.event_view)折进分析那一行:标题接上编号,副标题是判定语(算不了的就是「…没有做成:原因」, * tool.end 的 analysis(tools/analysis.event_view)折进分析那一行:标题接上这份 Agent 自己的编号(「本节点 A1」),副标题是
* 服务端写好的);算不了的用提醒色。老服务端没有这一段:preview 只是结论的前一截,判定语多半被截掉,只照参数写叫法,不从里面拼 * 判定语(算不了的就是「…没有做成:原因」,服务端写好的);算不了的用提醒色。老服务端没有这一段:preview 只是结论的前一截,
* 判定语多半被截掉,只照参数写叫法,不从里面拼
*/ */
function settleAnalysis(step: Step, d: Record<string, any>): void { function settleAnalysis(step: Step, d: Record<string, any>): void {
const view = d.analysis && typeof d.analysis === 'object' ? d.analysis as Record<string, any> : null const view = d.analysis && typeof d.analysis === 'object' ? d.analysis as Record<string, any> : null
@@ -1403,7 +1404,9 @@ function settleAnalysis(step: Step, d: Record<string, any>): void {
if (typeof view.reason_code === 'string') info.reasonCode = view.reason_code if (typeof view.reason_code === 'string') info.reasonCode = view.reason_code
} }
step.analysis = info step.analysis = info
step.title = ANALYSIS_TEXT.step.title(analysisMethodLabel(info.op, info.mode), info.alias) // Agent 工具那一步的编号是这份 Agent 自己的分析容器里的(交回模型的「【分析 A1】」),报告目录里的 A1 按整次运行的台账编,
// 两者不一定相同(图里还有分析卡时就不是):标题写「本节点 A1」,不冒充报告里的编号
step.title = ANALYSIS_TEXT.step.nodeTitle(analysisMethodLabel(info.op, info.mode), info.alias)
if (info.phrase) step.sub = step.sub ? `${info.phrase} · ${step.sub}` : info.phrase if (info.phrase) step.sub = step.sub ? `${info.phrase} · ${step.sub}` : info.phrase
if (info.status === 'refused') { if (info.status === 'refused') {
step.level = 'warn' step.level = 'warn'
+5 -5
View File
@@ -127,17 +127,17 @@ await section('分析:Agent 的分析工具、分析卡的每一项', async ()
&& wrong.sub === '调用有误,没有记成结论' && !!wrong.next?.includes('Agent 会按这条说明改正参数'), `${wrong?.status} ${wrong?.sub}`) && wrong.sub === '调用有误,没有记成结论' && !!wrong.next?.includes('Agent 会按这条说明改正参数'), `${wrong?.status} ${wrong?.sub}`)
check('……服务端的原话留在展开区,调用错误的代码记在这一行', !!wrong?.result?.includes('Q2 里没有「sales」这一列') check('……服务端的原话留在展开区,调用错误的代码记在这一行', !!wrong?.result?.includes('Q2 里没有「sales」这一列')
&& wrong.analysis?.callError === 'unknown_column', wrong?.result) && wrong.analysis?.callError === 'unknown_column', wrong?.result)
check('做成的:标题接上本节点的编号(A1,和交回模型的「【分析 A1】」同一个)', decompose?.title === '变化拆解(可加) · A1', decompose?.title) check('做成的:标题接上本节点的编号(A1,和交回模型的「【分析 A1】」同一个)', decompose?.title === '变化拆解(可加) · 本节点 A1', decompose?.title)
check('……副标题是服务端渲染的判定语', decompose?.sub === '销售额下降 340(-12.6%),主要来自华东(-300,占 88.2%)', decompose?.sub) check('……副标题是服务端渲染的判定语', decompose?.sub === '销售额下降 340(-12.6%),主要来自华东(-300,占 88.2%)', decompose?.sub)
check('……结论工件可下钻,Agent 写的假设另记、不进参数', !!decompose?.artifact && decompose.analysis?.hypothesis === '下降是不是集中在某个地区' check('……结论工件可下钻,Agent 写的假设另记、不进参数', !!decompose?.artifact && decompose.analysis?.hypothesis === '下降是不是集中在某个地区'
&& !decompose.detail?.includes('hypothesis') && !decompose.detail?.includes('null'), decompose?.detail?.slice(0, 80)) && !decompose.detail?.includes('hypothesis') && !decompose.detail?.includes('null'), decompose?.detail?.slice(0, 80))
check('……preview(结论 JSON 的前一截)不摆出来', !decompose?.result, decompose?.result?.slice(0, 40)) check('……preview(结论 JSON 的前一截)不摆出来', !decompose?.result, decompose?.result?.slice(0, 40))
check('算不了的:一条结论(不是失败),提醒色,判定语就是「…没有做成:原因」', baseline?.status === 'done' && baseline.level === 'warn' check('算不了的:一条结论(不是失败),提醒色,判定语就是「…没有做成:原因」', baseline?.status === 'done' && baseline.level === 'warn'
&& baseline.title === '波动基线 · A2' && baseline.sub === '波动基线没有做成:只有 0 期历史,至少要 8 期', `${baseline?.title} ${baseline?.sub}`) && baseline.title === '波动基线 · 本节点 A2' && baseline.sub === '波动基线没有做成:只有 0 期历史,至少要 8 期', `${baseline?.title} ${baseline?.sub}`)
check('……下一步说能照实写原因、不能据此下判断', !!baseline?.next?.includes('算不了也是一条结论')) check('……下一步说能照实写原因、不能据此下判断', !!baseline?.next?.includes('算不了也是一条结论'))
check('……紧跟着的「没有做成」提醒折进这一行,不另起一行', !all.some((s) => s.code === 'analysis_refused'), check('……紧跟着的「没有做成」提醒折进这一行,不另起一行', !all.some((s) => s.code === 'analysis_refused'),
all.filter((s) => s.code === 'analysis_refused').map((s) => s.title).join(' | ')) all.filter((s) => s.code === 'analysis_refused').map((s) => s.title).join(' | '))
check('可比性检查也有编号和判定语', comparable?.title === '可比性检查 · A3' && !!comparable.sub?.startsWith('两期可比'), comparable?.title) check('可比性检查也有编号和判定语', comparable?.title === '可比性检查 · 本节点 A3' && !!comparable.sub?.startsWith('两期可比'), comparable?.title)
check('分析那几行是正常的子步骤:不当成运行出错(运行照样完成)', all.some((s) => s.kind === 'lifecycle' && s.title === '完成')) check('分析那几行是正常的子步骤:不当成运行出错(运行照样完成)', all.some((s) => s.kind === 'lifecycle' && s.title === '完成'))
const claims = all.find((s) => s.code === 'report_analysis_claims') const claims = all.find((s) => s.code === 'report_analysis_claims')
check('报告的分析说法重写后仍有:说成人话(几处、已在报告中标注),副标题点名判断词', claims?.title === '报告有 3 处分析说法未通过核对,已在报告中标注' check('报告的分析说法重写后仍有:说成人话(几处、已在报告中标注),副标题点名判断词', claims?.title === '报告有 3 处分析说法未通过核对,已在报告中标注'
@@ -170,8 +170,8 @@ await section('分析:Agent 的分析工具、分析卡的每一项', async ()
// 协作成员的分析:编号是成员自己的,副标题接着写是谁调用的 // 协作成员的分析:编号是成员自己的,副标题接着写是谁调用的
const team = flatten(mod.decodeRun(synthetic.analysisTeamRun(), { status: 'succeeded' })).filter((s) => s.kind === 'analysis') const team = flatten(mod.decodeRun(synthetic.analysisTeamRun(), { status: 'succeeded' })).filter((s) => s.kind === 'analysis')
check('协作成员调分析工具:编号是成员自己的,副标题写判定语和谁调用的', team[0]?.title === '变化拆解(可加) · A1' check('协作成员调分析工具:不带编号(成员容器里的 A1 不是报告目录里的),副标题写判定语和谁调用的', team[0]?.title === '变化拆解(可加)'
&& team[0].sub === `${synthetic.TEAM_PHRASE} · 分析员 调用` && team[1]?.title === '波动基线 · A2', team.map((s) => `${s.title}:${s.sub}`).join(' | ')) && team[0].sub === `${synthetic.TEAM_PHRASE} · 分析员 调用` && team[1]?.title === '波动基线', team.map((s) => `${s.title}:${s.sub}`).join(' | '))
}) })
await section('合并查询', async () => { await section('合并查询', async () => {
+3 -2
View File
@@ -748,7 +748,7 @@ await section('分析:Agent 的分析工具、分析卡的每一项一行,
&& await page.locator('[data-step-kind="analysis"]:not([data-step-status="failed"]) svg.lucide-chart-column-stacked').count() === 5, && await page.locator('[data-step-kind="analysis"]:not([data-step-status="failed"]) svg.lucide-chart-column-stacked').count() === 5,
String(await rows.count())) String(await rows.count()))
const line = async (i) => (await rows.nth(i).innerText().catch(() => '')).replace(/\s+/g, ' ') const line = async (i) => (await rows.nth(i).innerText().catch(() => '')).replace(/\s+/g, ' ')
check('做成的一行:标题写算子的叫法和编号,副标题是判定语', (await line(1)).includes('变化拆解(可加) · A1') check('做成的一行:标题写算子的叫法和本节点的编号(报告目录里的编号另算),副标题是判定语', (await line(1)).includes('变化拆解(可加) · 本节点 A1')
&& (await line(1)).includes('主要来自华东(-300,占 88.2%)'), await line(1)) && (await line(1)).includes('主要来自华东(-300,占 88.2%)'), await line(1))
check('……标题里不出现工具名 analysis_decompose', !(await page.locator('[data-turn]').first().innerText()).includes('analysis_decompose')) check('……标题里不出现工具名 analysis_decompose', !(await page.locator('[data-turn]').first().innerText()).includes('analysis_decompose'))
await rows.nth(1).locator('button').first().click() await rows.nth(1).locator('button').first().click()
@@ -790,7 +790,8 @@ await section('分析:Agent 的分析工具、分析卡的每一项一行,
const team = await open('syn=analysis-team', { w: 1100, h: 900 }) const team = await open('syn=analysis-team', { w: 1100, h: 900 })
const tl = (await team.page.locator('[data-step-kind="analysis"]').first().innerText().catch(() => '')).replace(/\s+/g, ' ') const tl = (await team.page.locator('[data-step-kind="analysis"]').first().innerText().catch(() => '')).replace(/\s+/g, ' ')
check('协作成员的分析:编号是成员自己的,副标题接着写是谁调用的', tl.includes('变化拆解(可加) · A1') && tl.includes('分析员 调用'), tl) check('协作成员的分析:不带编号(进不了台账,报告目录里的 A1 另算),副标题接着写是谁调用的', tl.includes('变化拆解(可加)') && !tl.includes('本节点 A1')
&& tl.includes('分析员 调用'), tl)
await team.page.close() await team.page.close()
}) })