From 5617deb80bfda12183a2d40c714f7e2cc8f8130c Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Wed, 21 Oct 2026 18:08:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=86=E6=9E=90=E6=96=B9=E6=B3=95=E5=AE=9A?= =?UTF-8?q?=E7=A8=BF=EF=BC=88ANALYSIS=5FMETHOD=5FPROMPT=EF=BC=8Cguide=20id?= =?UTF-8?q?=20=E5=8D=87=E5=88=B0=20analysis.method@2=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E5=9C=A8=E7=AC=AC=E4=B8=80=E7=89=88=E5=85=AD=E6=9D=A1=E4=B8=8A?= =?UTF-8?q?=E5=86=99=E6=B8=85=E6=8C=89=E9=97=AE=E9=A2=98=E9=80=89=E5=9F=BA?= =?UTF-8?q?=E6=9C=9F=EF=BC=88=E7=8E=AF=E6=AF=94=E6=AF=94=E4=B8=8A=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E6=95=B4=E6=9C=9F=E3=80=81=E5=90=8C=E6=AF=94=E6=AF=94?= =?UTF-8?q?=E5=8E=BB=E5=B9=B4=E5=90=8C=E6=9C=9F=E5=B9=B6=E5=86=99=20lag?= =?UTF-8?q?=E3=80=81=E5=92=8C=E7=9B=AE=E6=A0=87=E6=AF=94=E8=A6=81=E5=BA=93?= =?UTF-8?q?=E9=87=8C=E6=9C=89=E7=9B=AE=E6=A0=87=EF=BC=8C=E6=B2=A1=E8=AF=B4?= =?UTF-8?q?=E5=B0=B1=E6=AF=94=E4=B8=8A=E4=B8=80=E4=B8=AA=E6=95=B4=E6=9C=9F?= =?UTF-8?q?=EF=BC=8C=E6=9C=AC=E6=9C=9F=E5=8F=96=E6=9C=80=E8=BF=91=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E8=BF=87=E5=AE=8C=E7=9A=84=E6=95=B4=E6=9C=9F=EF=BC=89?= =?UTF-8?q?=EF=BC=9B=E5=91=A8=E5=BA=8F=E5=88=97=E7=9A=84=E6=9C=9F=E6=AC=A1?= =?UTF-8?q?=E5=9C=A8=20SQL=20=E9=87=8C=E7=94=B1=E6=97=A5=E6=9C=9F=E5=88=97?= =?UTF-8?q?=E7=AE=97=E5=87=BA=E6=9D=A5=EF=BC=8C=E5=86=99=E6=AF=8F=E5=91=A8?= =?UTF-8?q?=E7=AC=AC=E4=B8=80=E5=A4=A9=E7=9A=84=E6=97=A5=E6=9C=9F=EF=BC=88?= =?UTF-8?q?=E9=99=84=20SQLite=20=E5=86=99=E6=B3=95=EF=BC=89=E6=88=96=20ISO?= =?UTF-8?q?=20=E5=91=A8=EF=BC=8CSQLite=20=E7=9A=84=20%W=E3=80=81MySQL=20?= =?UTF-8?q?=E7=9A=84=20%u=20=E4=B8=8D=E6=98=AF=20ISO=20=E5=91=A8=EF=BC=8C?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E5=8F=96=E7=8E=B0=E6=88=90=E7=9A=84=E5=91=A8?= =?UTF-8?q?=E5=8F=B7=E6=96=87=E6=9C=AC=E6=9C=AC=E6=9C=9F=E9=BD=90=E4=B8=8D?= =?UTF-8?q?=E9=BD=90=E6=A0=B8=E5=AF=B9=E4=B8=8D=E4=BA=86=EF=BC=9B=E5=85=88?= =?UTF-8?q?=E5=8F=AF=E6=AF=94=E6=80=A7=E4=B8=8E=E5=9F=BA=E7=BA=BF=E3=80=81?= =?UTF-8?q?=E8=B6=8B=E5=8A=BF=EF=BC=8C=E5=86=8D=E6=8B=86=E5=9B=A0=E5=AD=90?= =?UTF-8?q?=E4=B8=8E=E7=BB=93=E6=9E=84=E3=80=81=E6=8C=89=E7=BB=B4=E5=BA=A6?= =?UTF-8?q?=E7=94=B1=E7=B2=97=E5=88=B0=E7=BB=86=EF=BC=8C=E6=89=BE=E5=88=B0?= =?UTF-8?q?=E4=B8=BB=E8=A6=81=E6=9D=A5=E6=BA=90=E5=86=8D=E4=B8=8B=E9=92=BB?= =?UTF-8?q?=E4=B8=80=E5=B1=82=EF=BC=9B=E6=AF=8F=E6=AC=A1=E5=86=99=E6=98=8E?= =?UTF-8?q?=E5=9C=A8=E9=AA=8C=E8=AF=81=E4=BB=80=E4=B9=88=EF=BC=8C=E6=8B=86?= =?UTF-8?q?=E4=BA=86=E4=B8=8D=E9=9B=86=E4=B8=AD=E4=B9=9F=E6=98=AF=E7=BB=93?= =?UTF-8?q?=E8=AE=BA=EF=BC=9B=E4=B8=8D=E4=BD=9C=E5=9B=A0=E6=9E=9C=E6=96=AD?= =?UTF-8?q?=E8=A8=80=EF=BC=9B=E9=98=88=E5=80=BC=E4=B8=8D=E7=94=B1=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=AE=9A=E3=80=81=E7=AE=97=E4=B8=8D=E4=BA=86=E7=85=A7?= =?UTF-8?q?=E5=AE=9E=E8=AF=B4=E3=80=81=E8=B0=83=E7=94=A8=E5=86=99=E9=94=99?= =?UTF-8?q?=E7=85=A7=E8=AF=B4=E6=98=8E=E6=94=B9=EF=BC=9B=E6=9C=80=E5=90=8E?= =?UTF-8?q?=E4=BA=A4=E7=BB=99=E5=86=99=E4=BD=9C=E8=80=85=E7=9A=84=E6=96=87?= =?UTF-8?q?=E5=AD=97=E9=87=8C=E5=88=97=E5=87=BA=E5=81=9A=E8=BF=87=E7=9A=84?= =?UTF-8?q?=E5=88=86=E6=9E=90=EF=BC=88An=EF=BC=89=E5=92=8C=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E5=8E=9F=E8=AF=9D=E3=80=82=E6=B5=8B=E8=AF=95=E5=AE=88?= =?UTF-8?q?=E8=BF=99=E5=87=A0=E4=BB=B6=E4=BA=8B=E9=83=BD=E5=9C=A8=E3=80=81?= =?UTF-8?q?=E7=82=B9=E5=90=8D=E7=9A=84=E5=B7=A5=E5=85=B7=E6=98=AF=E9=82=A3?= =?UTF-8?q?=E4=BA=94=E4=B8=AA=E3=80=81=E4=BA=94=E4=B8=AA=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E5=85=B1=E7=94=A8=E8=BF=99=E4=B8=80=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/tools/builtin/analysis.py | 18 ++++---- backend/tests/test_analysis_method_guide.py | 47 +++++++++++++++++++++ 2 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 backend/tests/test_analysis_method_guide.py diff --git a/backend/app/tools/builtin/analysis.py b/backend/app/tools/builtin/analysis.py index 89213a2a..43efd152 100644 --- a/backend/app/tools/builtin/analysis.py +++ b/backend/app/tools/builtin/analysis.py @@ -22,20 +22,22 @@ from app.tools.registry import ToolContext, ToolGuide, register CATEGORY = "分析" -#: 「分析方法」:五个分析工具共用,绑了任一个就注入 system(第一版,第 2 波定稿)。改了方法就改 id 里的版本号:评测的 +#: 「分析方法」:五个分析工具共用,绑了任一个就注入 system(第 2 波定稿为第二版)。改了方法就改 id 里的版本号:评测的 #: prompt_fingerprint 认得出提示词变了 ANALYSIS_METHOD_PROMPT = """\ # 分析方法(平台内置,用分析工具时照这个做) -1. 先定现象:取本期和问题对应的基期,用 analysis_comparable 确认两期能直接比;历史够长(缺省至少 8 期)就用 analysis_baseline 放回历史看——是常规波动就照实说,不硬找原因。 -2. 指标是乘积或比率时,先用 analysis_decompose 的 factor、ratio 模式拆因子、拆结构。 -3. 按数据目录里的候选维度由粗到细拆(additive 模式),每次用 headline 给出总数核对拆开后是否闭合;找到主要来源再用 parent 下钻一层:下钻的 SQL 由你写(只查那一段),系统按上一层那一段的值核对闭合。 -4. 每次调用在 hypothesis 里写明这次在验证什么;拆了没发现集中也是结论,照实写。 -5. 不作因果断言:数据只说明变化来自哪里;原因要有知识库原话或输入作依据,否则写成待核实的推测。 -6. 阈值和方法由平台定,工具参数里没有阈值;算不了时照实说算不了的原因,不要换个说法硬下判断。 +1. 先按问题定基期:「这周怎么样」「环比」比上一个整期(上周、上月);「同比」「和去年比」比去年同期(波动基线的 lag 周数据写 52、月数据写 12,历史要相应够长);「和目标比」比库里的目标或预算(目标和实际查进同一份结果,用一列区分两者当期次,label 写清是和目标比;库里没有目标就照实说,不自己定目标)。问题没说时比上一个整期,并写明比的是哪一期。本期取最近一个已经过完的整期:这周没过完,就比上周和上上周。 +2. 期次在 SQL 里由日期列算出来:按周汇总写每周第一天的日期(SQLite 写 date(日期列, '-6 days', 'weekday 1')),或 ISO 周「YYYY-Www」——SQLite 的 %W、MySQL 的 %u 算的不是 ISO 周,同一个「2026-W42」落在别的日子上。直接取表里现成的周号文本时,系统认不出它的算法,本期齐不齐核对不了。 +3. 先定现象:用 analysis_comparable 确认两期能直接比;历史够长(缺省至少 8 期)就用 analysis_baseline 放回历史看,要看是不是持续在变用 analysis_trend。是常规波动就照实说,不硬找原因。 +4. 再拆:指标是乘积(销售额 = 订单数 × 客单价)先用 analysis_decompose 的 factor 拆因子,是比率(退款率、客单价)先用 ratio 拆结构和各段自身的变化;可加的量按数据目录里的候选维度由粗到细用 additive 拆,每次用 headline 给出总数,核对拆开后是否闭合。找到主要来源再用 parent 下钻一层:下钻的 SQL 由你写(只查那一段),系统按上一层那一段的值核对闭合。 +5. 每次调用在 hypothesis 里写明这次在验证什么。拆了没发现集中(分散、持平)也是结论,照实写,不换着维度硬凑「主要来自」。两组、两期的差异要说站不站得住时用 analysis_differ。 +6. 不作因果断言:数据只说明变化来自哪里,说明不了为什么。原因要有知识库原话或运行输入作依据,否则写成待核实的推测。 +7. 阈值和方法由平台定,工具参数里没有阈值。算不了(历史不够、本期不完整、截断、不可比)时照实说原因,不要换个说法硬下判断;调用写错时照交回的说明改了再调。 +8. 最后交给写作者的文字里列出做过的分析:「【分析 An】」的编号、分析的是什么、判定(判定语原话)或算不了的原因,没做的分析不要写成做过。 分析结论的编号「【分析 An】」只在本节点有效:下钻时 parent 写它(如 {"of": "A1", "key": ["华东"]})。判定语由系统写好了,结论里照它的意思说,不要改写判断的强弱。""" -METHOD_GUIDE = ToolGuide(id="analysis.method@1", text=ANALYSIS_METHOD_PROMPT) +METHOD_GUIDE = ToolGuide(id="analysis.method@2", text=ANALYSIS_METHOD_PROMPT) #: 交回 agent 的说法:这个节点里没有可分析的查询容器(调用工具节点、工具库试用、没记台账的老运行) NO_SCOPE = ("分析工具只能由 Agent 在本节点查过数之后调用:它分析的是本节点交回过的查询结果(【证据 Qn】的编号)。" diff --git a/backend/tests/test_analysis_method_guide.py b/backend/tests/test_analysis_method_guide.py new file mode 100644 index 00000000..0c937ac8 --- /dev/null +++ b/backend/tests/test_analysis_method_guide.py @@ -0,0 +1,47 @@ +"""平台内置的分析方法(tools/builtin/analysis.py 的 ANALYSIS_METHOD_PROMPT,第 2 波定稿,guide id analysis.method@2)。 + +方法随代码升版、绑了任一个分析工具就注入 Agent 的 system(注入本身见 tests/test_analysis_tools.py)。这里守方法里该写清的几件事 +都在:按问题选基期、周序列的期次怎么写、先现象后拆解再下钻的顺序、写明在验证什么、拆了不集中也是结论、不作因果断言、阈值 +不由模型定、算不了照实说、最后列出做过的分析;方法里点名的工具都是真有的那五个。 +""" +from __future__ import annotations + +import re + +from app.analysis import contract +from app.tools.builtin.analysis import ANALYSIS_METHOD_PROMPT, METHOD_GUIDE +from app.tools.registry import all_specs + + +def test_the_guide_is_the_second_version_and_shared_by_all_five_tools(): + assert METHOD_GUIDE.id == "analysis.method@2" and METHOD_GUIDE.text == ANALYSIS_METHOD_PROMPT + specs = all_specs() + assert {name for name, spec in specs.items() if spec.guide is METHOD_GUIDE} == set(contract.TOOL_NAMES.values()) + + +def test_the_method_says_how_to_pick_the_base_period(): + first = ANALYSIS_METHOD_PROMPT.split("\n2.")[0] + for words in ("上周", "去年同期", "和目标比", "上一个整期", "lag", "52", "库里没有目标就照实说", "已经过完的整期"): + assert words in first, words + + +def test_the_method_says_how_to_write_weekly_periods(): + for words in ("每周第一天的日期", "ISO 周「YYYY-Www」", "SQLite 的 %W", "不是 ISO 周", "date(日期列, '-6 days', 'weekday 1')", + "核对不了"): + assert words in ANALYSIS_METHOD_PROMPT, words + + +def test_the_method_orders_the_steps_and_names_only_real_tools(): + text = ANALYSIS_METHOD_PROMPT + # 先可比性和基线,再拆因子与结构,再按维度由粗到细,找到主要来源再下钻一层 + order = [text.index(w) for w in ("analysis_comparable", "analysis_baseline", "factor", "ratio", "additive", "parent")] + assert order == sorted(order), order + named = set(re.findall(r"\banalysis_[a-z]+\b", text)) + assert named == set(contract.TOOL_NAMES.values()), named + assert "由粗到细" in text and "下钻一层" in text and "headline" in text + + +def test_the_method_keeps_the_round_rules(): + for words in ("hypothesis", "拆了没发现集中", "也是结论", "不作因果断言", "待核实的推测", "工具参数里没有阈值", + "算不了", "照实说原因", "【分析 An】", "判定语原话", "没做的分析不要写成做过"): + assert words in ANALYSIS_METHOD_PROMPT, words