From bca19fd845cf28955cdb3e2f6e23017c3613fcfd Mon Sep 17 00:00:00 2001 From: hub-gif <2487812171@qq.com> Date: Tue, 14 Apr 2026 10:26:55 +0800 Subject: [PATCH] test(pipeline): run llm_keyword_suggest against real LLM when keys set Remove mock; load market_assistant/.env before checking credentials. Live test skips without OPENAI_* or LLM_* key+base URL. Made-with: Cursor --- .../tests/test_llm_keyword_suggest.py | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 backend/pipeline/tests/test_llm_keyword_suggest.py diff --git a/backend/pipeline/tests/test_llm_keyword_suggest.py b/backend/pipeline/tests/test_llm_keyword_suggest.py new file mode 100644 index 0000000..172049c --- /dev/null +++ b/backend/pipeline/tests/test_llm_keyword_suggest.py @@ -0,0 +1,110 @@ +"""llm_keyword_suggest:分块/解析烟测;有 API 配置时直连大模型做联调。""" +from __future__ import annotations + +import os +import unittest +from pathlib import Path + +try: + from dotenv import load_dotenv + + _ma_env = Path(__file__).resolve().parents[3] / ".env" + if _ma_env.is_file(): + load_dotenv(_ma_env) +except ImportError: + pass + +from django.test import SimpleTestCase + +from pipeline.llm_keyword_suggest import ( + MAX_CHUNK_CHARS, + MAX_CHUNKS, + _chunk_comment_texts, + _parse_phrases_object, + suggest_focus_keywords_from_all_comments, +) + + +def _llm_configured() -> bool: + key = (os.environ.get("OPENAI_API_KEY") or os.environ.get("LLM_API_KEY") or "").strip() + base = (os.environ.get("OPENAI_BASE_URL") or os.environ.get("LLM_BASE_URL") or "").strip() + return bool(key and base) + + +class ChunkCommentTextsTests(SimpleTestCase): + def test_empty(self) -> None: + self.assertEqual(_chunk_comment_texts([]), []) + + def test_respects_max_chunk_chars(self) -> None: + a = "x" * (MAX_CHUNK_CHARS // 2) + b = "y" * (MAX_CHUNK_CHARS // 2) + c = "z" * (MAX_CHUNK_CHARS // 2) + parts = _chunk_comment_texts([a, b, c]) + self.assertGreaterEqual(len(parts), 2) + for p in parts: + self.assertLessEqual(len(p) + p.count("\n"), MAX_CHUNK_CHARS + 50) + + def test_max_chunks_trims(self) -> None: + texts = [f"段落{i} " + "字" * 800 for i in range(80)] + parts = _chunk_comment_texts(texts) + self.assertLessEqual(len(parts), MAX_CHUNKS) + + +class ParsePhrasesObjectTests(SimpleTestCase): + def test_plain_json(self) -> None: + raw = '{"phrases": ["低糖", "口感好"]}' + self.assertEqual(_parse_phrases_object(raw), ["低糖", "口感好"]) + + def test_fenced_json(self) -> None: + raw = '```json\n{"phrases": ["A", "B"]}\n```' + self.assertEqual(_parse_phrases_object(raw), ["A", "B"]) + + def test_embedded_object(self) -> None: + raw = '前缀 {"phrases": ["x"]} 后缀' + self.assertEqual(_parse_phrases_object(raw), ["x"]) + + def test_invalid_returns_empty(self) -> None: + self.assertEqual(_parse_phrases_object("not json"), []) + + +class SuggestFocusKeywordsTests(SimpleTestCase): + def test_no_comments_returns_empty(self) -> None: + out = suggest_focus_keywords_from_all_comments( + keyword="低GI", + brief_slice={"comment_focus_keywords": []}, + all_comment_texts=[], + ) + self.assertEqual(out["suggested_focus_keywords"], []) + self.assertEqual(out["chunks_processed"], 0) + self.assertIn("无评价", out["rationale"]) + + +@unittest.skipUnless( + _llm_configured(), + "需要环境变量 OPENAI_API_KEY+OPENAI_BASE_URL(或 LLM_API_KEY+LLM_BASE_URL)," + "与 AI_crawler 相同;可在 market_assistant/.env 配置后重跑。", +) +class SuggestFocusKeywordsLiveLLMTests(SimpleTestCase): + """直连网关调用 ``chat_completion_text``,会消耗少量 token。""" + + def test_live_extracts_phrases_from_comments(self) -> None: + comments = [ + "低GI饼干口感偏硬,甜度刚好,饱腹感不错。", + "物流有点慢,包装压扁了一角,但味道还行。", + "希望出小包装,一次吃不完容易受潮。", + ] + out = suggest_focus_keywords_from_all_comments( + keyword="低GI饼干", + brief_slice={"comment_focus_keywords": [{"word": "甜度"}]}, + all_comment_texts=comments, + ) + self.assertGreaterEqual(out["chunks_processed"], 1) + self.assertEqual(out["total_comment_texts"], 3) + kws = out["suggested_focus_keywords"] + self.assertIsInstance(kws, list) + self.assertGreater(len(kws), 0, "模型应返回至少 1 条短语") + for p in kws: + self.assertIsInstance(p, str) + self.assertGreaterEqual(len(p), 2) + self.assertLessEqual(len(p), 24) + self.assertNotIn("甜度", kws)