返回 DeepSeek-Reasonix
reasoning_language.go
根目录 / internal / agent / reasoning_language.go
1 package agent
2
3 import (
4 "context"
5 "strings"
6 "unicode"
7 )
8
9 type reasoningLanguageContextKey struct{}
10 type responseLanguageContextKey struct{}
11
12 // NormalizeReasoningLanguage returns one of auto|zh|en for runtime-only visible
13 // reasoning preferences. Keep this local to agent so sub-agents can inherit the
14 // preference without depending on config.
15 func NormalizeReasoningLanguage(lang string) string {
16 switch strings.ToLower(strings.TrimSpace(lang)) {
17 case "zh", "cn", "chinese", "中文":
18 return "zh"
19 case "en", "english":
20 return "en"
21 default:
22 return "auto"
23 }
24 }
25
26 // NormalizeResponseLanguage returns one of auto|zh|en for final-answer language
27 // preferences. Auto keeps the stable same-as-user language policy.
28 func NormalizeResponseLanguage(lang string) string {
29 switch strings.ToLower(strings.TrimSpace(lang)) {
30 case "zh", "cn", "chinese", "中文":
31 return "zh"
32 case "en", "english":
33 return "en"
34 default:
35 return "auto"
36 }
37 }
38
39 // ResponseLanguageBlock is transient user-turn context for final answers. It
40 // stays out of the stable system prompt so changing the preference between turns
41 // does not churn the cached prefix.
42 func ResponseLanguageBlock(lang string) string {
43 switch NormalizeResponseLanguage(lang) {
44 case "zh":
45 return "<response-language>\nFinal answer language preference: use Simplified Chinese for user-facing replies unless the user explicitly asks for another language. Keep code, identifiers, file paths, shell commands, and untranslated technical terms in their original form.\n</response-language>"
46 case "en":
47 return "<response-language>\nFinal answer language preference: use English for user-facing replies unless the user explicitly asks for another language. Keep code, identifiers, file paths, shell commands, and untranslated technical terms in their original form.\n</response-language>"
48 default:
49 return ""
50 }
51 }
52
53 // ReasoningLanguageBlock is transient user-turn context. It deliberately does
54 // not belong in the stable system prompt or tool schemas.
55 func ReasoningLanguageBlock(lang string) string {
56 switch NormalizeReasoningLanguage(lang) {
57 case "zh":
58 // Imperative wording measured against soft "偏好……请使用" phrasing:
59 // the soft form loses the first reasoning segment on Chinese prompts
60 // that embed English logs/code, and the first segment anchors the
61 // whole turn once providers round-trip prior reasoning.
62 return "<reasoning-language>\n必须使用简体中文书写全部可见思考/推理文本:从第一个字开始就用中文,并在整轮内保持中文,即使系统提示词、工具说明、工具输出或引用的代码是英文。代码、标识符、文件路径、shell 命令和未翻译的技术术语保持原文。此要求只约束可见思考文本,不覆盖用户对最终回答语言的明确要求。\n</reasoning-language>"
63 case "en":
64 return "<reasoning-language>\nVisible reasoning/thinking text preference: use English when the provider exposes reasoning text. Keep code, identifiers, file paths, shell commands, and untranslated technical terms in their original form. This preference does not override an explicit user request for the final answer language.\n</reasoning-language>"
65 default:
66 return ""
67 }
68 }
69
70 // ResolveReasoningLanguage returns the concrete visible-reasoning language for
71 // a turn. Explicit zh/en settings win; auto anchors clear Chinese user prompts
72 // and otherwise stays provider-default to preserve the historical no-injection
73 // behaviour for English and ambiguous turns.
74 func ResolveReasoningLanguage(lang, source string) string {
75 mode := NormalizeReasoningLanguage(lang)
76 if mode != "auto" {
77 return mode
78 }
79 return InferReasoningLanguageFromText(source)
80 }
81
82 // InferReasoningLanguageFromText conservatively detects Chinese user-authored
83 // turns for auto reasoning-language mode. It strips Reasonix-injected context
84 // wrappers first so large @file payloads or transient XML blocks do not drown
85 // out the user's actual prompt. English and ambiguous turns intentionally return
86 // auto, preserving the old no-extra-instruction behaviour.
87 func InferReasoningLanguageFromText(source string) string {
88 source = reasoningLanguageSourceText(source)
89 if source == "" {
90 return "auto"
91 }
92 han, cjkPunct := reasoningLanguageScriptCounts(source)
93 switch {
94 case han >= 4:
95 return "zh"
96 case han >= 2 && (cjkPunct > 0 || hasChineseReasoningCue(source)):
97 return "zh"
98 default:
99 return "auto"
100 }
101 }
102
103 func reasoningLanguageSourceText(source string) string {
104 s := strings.TrimSpace(StripTransientUserBlocks(source))
105 const preamble = "Referenced context:"
106 if !strings.HasPrefix(s, preamble) {
107 return s
108 }
109 s = strings.TrimSpace(s[len(preamble):])
110 for {
111 s = strings.TrimSpace(s)
112 if s == "" || !strings.HasPrefix(s, "<") {
113 return s
114 }
115 tagEnd := strings.IndexAny(s, " >\t\r\n")
116 if tagEnd <= 1 {
117 return s
118 }
119 tag := s[1:tagEnd]
120 switch tag {
121 case "file", "dir", "resource", "image":
122 closeTag := "</" + tag + ">"
123 i := strings.Index(s, closeTag)
124 if i < 0 {
125 return s
126 }
127 s = strings.TrimSpace(s[i+len(closeTag):])
128 default:
129 return s
130 }
131 }
132 }
133
134 func reasoningLanguageScriptCounts(source string) (han, cjkPunct int) {
135 for _, r := range source {
136 switch {
137 case unicode.In(r, unicode.Han):
138 han++
139 case isCJKPunctuation(r):
140 cjkPunct++
141 }
142 }
143 return han, cjkPunct
144 }
145
146 func isCJKPunctuation(r rune) bool {
147 switch {
148 case r >= 0x3000 && r <= 0x303F:
149 return true
150 case r >= 0xFF00 && r <= 0xFFEF:
151 return true
152 default:
153 return false
154 }
155 }
156
157 func hasChineseReasoningCue(source string) bool {
158 for _, cue := range chineseReasoningLanguageCues {
159 if strings.Contains(source, cue) {
160 return true
161 }
162 }
163 return false
164 }
165
166 var chineseReasoningLanguageCues = []string{
167 "你好", "请", "帮我", "帮忙", "看看", "看下", "解释", "说明", "总结", "分析",
168 "修复", "实现", "优化", "排查", "处理", "继续", "为什么", "怎么",
169 "是否", "能否", "支持", "设置", "中文", "思考", "问题", "报错",
170 "代码", "文件", "这个", "那个",
171 }
172
173 func reasoningLanguageBlockForSource(lang, source string) string {
174 return ReasoningLanguageBlock(ResolveReasoningLanguage(lang, source))
175 }
176
177 // WithResponseLanguage prefixes content with the transient response-language
178 // block unless the turn already starts with one.
179 func WithResponseLanguage(content, lang string) string {
180 block := ResponseLanguageBlock(lang)
181 if block == "" || hasLeadingInjectedBlock(content, "response-language") {
182 return content
183 }
184 return block + "\n\n" + content
185 }
186
187 // WithReasoningLanguage prefixes content with the transient reasoning-language
188 // block unless the turn already starts with an injected reasoning-language
189 // block. User-authored mentions of the tag later in the prompt must not suppress
190 // the configured preference.
191 func WithReasoningLanguage(content, lang string) string {
192 return WithReasoningLanguageForSource(content, lang, content)
193 }
194
195 // WithReasoningLanguageForSource prefixes content using source as the language
196 // signal for auto mode. Callers that expand @references should pass the raw
197 // user prompt as source so referenced English code or logs do not override the
198 // user's actual conversation language.
199 func WithReasoningLanguageForSource(content, lang, source string) string {
200 block := reasoningLanguageBlockForSource(lang, source)
201 if block == "" || hasLeadingInjectedBlock(content, "reasoning-language") {
202 return content
203 }
204 return block + "\n\n" + content
205 }
206
207 // hasLeadingInjectedBlock reports whether target is already among the transient
208 // blocks leading content, skipping past any other injected block on the way.
209 // It walks TransientUserBlockTags rather than a list of its own: when the two
210 // disagreed, a block the host had started injecting was treated as user prose
211 // and stopped the walk early, so an already-present target went undetected and
212 // was injected a second time.
213 func hasLeadingInjectedBlock(content, target string) bool {
214 s := strings.TrimLeft(content, " \t\r\n")
215 for {
216 if hasOpenTag(s, target) {
217 return strings.Contains(s, "</"+target+">")
218 }
219 skipped := false
220 for _, tag := range TransientUserBlockTags {
221 if tag == target || !hasOpenTag(s, tag) {
222 continue
223 }
224 rest, ok := trimLeadingTransientBlock(s, tag)
225 if !ok {
226 return false
227 }
228 s, skipped = rest, true
229 break
230 }
231 if !skipped {
232 return false
233 }
234 }
235 }
236
237 // hasOpenTag reports whether s opens with tag, with or without attributes
238 // (hook-context and capability-route carry them).
239 func hasOpenTag(s, tag string) bool {
240 return strings.HasPrefix(s, "<"+tag+">") || strings.HasPrefix(s, "<"+tag+" ")
241 }
242
243 func trimLeadingTransientBlock(content, tag string) (string, bool) {
244 closeTag := "</" + tag + ">"
245 i := strings.Index(content, closeTag)
246 if i < 0 {
247 return content, false
248 }
249 return strings.TrimLeft(content[i+len(closeTag):], " \t\r\n"), true
250 }
251
252 // WithResponseLanguagePreference carries the runtime final-answer language
253 // preference to spawned tools and sub-agents.
254 func WithResponseLanguagePreference(ctx context.Context, lang string) context.Context {
255 if ctx == nil {
256 ctx = context.Background()
257 }
258 return context.WithValue(ctx, responseLanguageContextKey{}, NormalizeResponseLanguage(lang))
259 }
260
261 // ResponseLanguageFromContext returns auto|zh|en.
262 func ResponseLanguageFromContext(ctx context.Context) string {
263 if ctx == nil {
264 return "auto"
265 }
266 if v, ok := ctx.Value(responseLanguageContextKey{}).(string); ok {
267 return NormalizeResponseLanguage(v)
268 }
269 return "auto"
270 }
271
272 // WithReasoningLanguagePreference carries the runtime preference to spawned
273 // tools, especially sub-agents whose first user turn is created outside the
274 // parent controller. It stores auto explicitly so live zh/en -> auto changes
275 // clear stale boot-time preferences in child paths.
276 func WithReasoningLanguagePreference(ctx context.Context, lang string) context.Context {
277 if ctx == nil {
278 ctx = context.Background()
279 }
280 return context.WithValue(ctx, reasoningLanguageContextKey{}, NormalizeReasoningLanguage(lang))
281 }
282
283 // ReasoningLanguageFromContext returns auto|zh|en.
284 func ReasoningLanguageFromContext(ctx context.Context) string {
285 if ctx == nil {
286 return "auto"
287 }
288 if v, ok := ctx.Value(reasoningLanguageContextKey{}).(string); ok {
289 return NormalizeReasoningLanguage(v)
290 }
291 return "auto"
292 }
293
293 lines GO