@@ -121,6 +121,9 @@ def _parse_csv_env(name: str, default: str) -> list[str]:
121121 "综合" , "综合解读" , "解读" , "突出" , "跨学科" , "联动" , "整合" , "对比" , "比较" ,
122122 "展开" , "展开讲讲" , "梳理" , "串联" , "理解" , "给出" , "提出" ,
123123}
124+ CHAT_HISTORY_MAX_MESSAGES = max (2 , int (os .getenv ("CHAT_HISTORY_MAX_MESSAGES" , "6" )))
125+ CHAT_HISTORY_TRUNCATED_CHARS = max (200 , int (os .getenv ("CHAT_HISTORY_TRUNCATED_CHARS" , "600" )))
126+ CHAT_HISTORY_FULL_TAIL_MESSAGES = max (2 , int (os .getenv ("CHAT_HISTORY_FULL_TAIL_MESSAGES" , "4" )))
124127
125128
126129def _compute_vector_source_fingerprint (text_limit : int ) -> tuple [Optional [int ], Optional [str ]]:
@@ -329,6 +332,11 @@ def _build_token_phrases(tokens: list[str], max_window: int = 4) -> set[str]:
329332 return phrases
330333
331334
335+ def _build_text_match_context (text : str | None ) -> tuple [str , set [str ]]:
336+ normalized_text = _normalize_match_text (text )
337+ return normalized_text , _build_token_phrases (_segment_text_tokens (normalized_text ))
338+
339+
332340@functools .lru_cache (maxsize = 2048 )
333341def _compile_non_cjk_term_pattern (term : str ):
334342 return re .compile (rf"(?<![0-9A-Za-z]){ re .escape (term )} (?![0-9A-Za-z])" , re .IGNORECASE )
@@ -344,9 +352,15 @@ def _concept_matches_text(concept: str, normalized_text: str, phrase_set: set[st
344352 return bool (_compile_non_cjk_term_pattern (concept ).search (normalized_text ))
345353
346354
347- def _present_terms_in_text (terms : list [str ], text : str ) -> list [str ]:
348- normalized_text = _normalize_match_text (text )
349- phrase_set = _build_token_phrases (_segment_text_tokens (normalized_text ))
355+ def _present_terms_in_text (
356+ terms : list [str ],
357+ text : str ,
358+ * ,
359+ normalized_text : str | None = None ,
360+ phrase_set : set [str ] | None = None ,
361+ ) -> list [str ]:
362+ if normalized_text is None or phrase_set is None :
363+ normalized_text , phrase_set = _build_text_match_context (text )
350364 present = []
351365 for term in terms :
352366 if _concept_matches_text (term , normalized_text , phrase_set ):
@@ -462,6 +476,21 @@ def _normalize_text_line(text: str | None) -> str:
462476 return re .sub (r"\s+" , " " , text or "" ).strip ()
463477
464478
479+ def _format_chat_history_lines (history : list [dict ] | None ) -> list [str ]:
480+ recent_messages = list ((history or [])[- CHAT_HISTORY_MAX_MESSAGES :])
481+ full_tail_start = max (0 , len (recent_messages ) - CHAT_HISTORY_FULL_TAIL_MESSAGES )
482+ history_lines = []
483+ for idx , msg in enumerate (recent_messages ):
484+ role = "用户" if msg .get ("role" ) == "user" else "助手"
485+ content = (msg .get ("content" ) or "" ).strip ()
486+ if not content :
487+ continue
488+ if idx < full_tail_start and len (content ) > CHAT_HISTORY_TRUNCATED_CHARS :
489+ content = content [:CHAT_HISTORY_TRUNCATED_CHARS ].rstrip () + "…"
490+ history_lines .append (f"{ role } : { content } " )
491+ return history_lines
492+
493+
465494def _load_ai_summary (con , chunk_id : int ) -> str :
466495 try :
467496 row = con .execute (
@@ -1008,12 +1037,7 @@ def _build_chat_context_payload(con, query: str, user_message: str, history: lis
10081037 for item in relation_hints
10091038 ]
10101039
1011- history_lines = []
1012- for msg in (history or [])[- 6 :]:
1013- role = "用户" if msg .get ("role" ) == "user" else "助手"
1014- content = (msg .get ("content" ) or "" ).strip ()
1015- if content :
1016- history_lines .append (f"{ role } : { content [:300 ]} " )
1040+ history_lines = _format_chat_history_lines (history )
10171041
10181042 summary = {
10191043 "subject_count" : len (groups ),
@@ -1071,11 +1095,7 @@ def _build_chat_context_for_request(query: str, user_message: str, history: list
10711095def _build_chat_prompt (query : str , user_message : str , context_payload : dict , history : list [dict ] | None = None ) -> str :
10721096 history_text = (context_payload .get ("history_text" ) or "" ).strip ()
10731097 if history and not history_text :
1074- history_text = "\n " .join (
1075- f"{ '用户' if msg .get ('role' ) == 'user' else '助手' } : { (msg .get ('content' ) or '' ).strip ()[:300 ]} "
1076- for msg in history [- 6 :]
1077- if (msg .get ("content" ) or "" ).strip ()
1078- )
1098+ history_text = "\n " .join (_format_chat_history_lines (history ))
10791099 if not history_text :
10801100 history_text = "(无)"
10811101
@@ -1805,17 +1825,12 @@ def related(
18051825
18061826
18071827@app .post ("/api/chat/context" )
1808- def chat_context (payload : dict = Body (...)):
1828+ async def chat_context (payload : dict = Body (...)):
18091829 """Build grounded context for AI chat before calling an external model service."""
18101830 query = str (payload .get ("query" , "" )).strip ()
18111831 user_message = str (payload .get ("user_message" , "" )).strip ()
18121832 history = payload .get ("history" ) or []
1813-
1814- con = get_db ()
1815- try :
1816- return _build_chat_context_payload (con , query , user_message , history = history )
1817- finally :
1818- con .close ()
1833+ return await run_in_threadpool (_build_chat_context_for_request , query , user_message , history )
18191834
18201835
18211836@app .post ("/api/chat/log" )
@@ -2101,11 +2116,12 @@ def _expand_cross_subject(concepts: list[dict], con) -> list[str]:
21012116
21022117
21032118def _score_result (result_text : str , query_terms : list [str ],
2104- matched_concepts : list [str ], is_same_subject : bool ) -> int :
2119+ matched_concepts : list [str ], is_same_subject : bool ,
2120+ * , normalized_text : str | None = None , phrase_set : set [str ] | None = None ) -> int :
21052121 """Compute relevance score 0-100 with IDF-weighted term importance."""
21062122 score = 0
2107- normalized_text = _normalize_match_text ( result_text )
2108- phrase_set = _build_token_phrases ( _segment_text_tokens ( normalized_text ) )
2123+ if normalized_text is None or phrase_set is None :
2124+ normalized_text , phrase_set = _build_text_match_context ( result_text )
21092125
21102126 # Term overlap — weight longer/rarer terms higher (max 35 points)
21112127 term_hits = 0
@@ -2298,10 +2314,22 @@ def gaokao_link(
22982314 scoring_concepts = list (dict .fromkeys (precomputed_terms [:6 ] + concept_names [:8 ]))
22992315 for r , link_type in all_results :
23002316 r_text = r ["text" ] or ""
2317+ normalized_text , phrase_set = _build_text_match_context (r_text )
23012318 # Find which concepts matched in this result
2302- r_matched = _present_terms_in_text (scoring_concepts , r_text )
2303- score = _score_result (r_text , scoring_terms , scoring_concepts ,
2304- r ["subject" ] == q_subject )
2319+ r_matched = _present_terms_in_text (
2320+ scoring_concepts ,
2321+ r_text ,
2322+ normalized_text = normalized_text ,
2323+ phrase_set = phrase_set ,
2324+ )
2325+ score = _score_result (
2326+ r_text ,
2327+ scoring_terms ,
2328+ scoring_concepts ,
2329+ r ["subject" ] == q_subject ,
2330+ normalized_text = normalized_text ,
2331+ phrase_set = phrase_set ,
2332+ )
23052333 # Skip results below minimum quality threshold
23062334 if score < 15 :
23072335 continue
@@ -2434,8 +2462,21 @@ def textbook_links(
24342462 results = []
24352463 for r in rows :
24362464 r_text = r ["text" ] or ""
2437- r_matched = _present_terms_in_text (concept_names , r_text )
2438- score = _score_result (r_text , top_terms , concept_names , False )
2465+ normalized_text , phrase_set = _build_text_match_context (r_text )
2466+ r_matched = _present_terms_in_text (
2467+ concept_names ,
2468+ r_text ,
2469+ normalized_text = normalized_text ,
2470+ phrase_set = phrase_set ,
2471+ )
2472+ score = _score_result (
2473+ r_text ,
2474+ top_terms ,
2475+ concept_names ,
2476+ False ,
2477+ normalized_text = normalized_text ,
2478+ phrase_set = phrase_set ,
2479+ )
24392480 results .append ({
24402481 "id" : r ["id" ],
24412482 "subject" : r ["subject" ],
0 commit comments