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