mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 19:45:08 +08:00
fix(agent): break self-replicating 400, narration, args truncation, queue drop
A series of cross-cutting stability fixes that surfaced together
during a long debugging session.
reasoning_content / Claude prefill self-replicating 400:
- ChatController persists typed errors (content starts with '[错误] ')
with status='error', so the failure text stops being re-sent as
multi-turn context — DeepSeek thinking 400 ('reasoning_content
must be passed back') and Claude 400 ('does not support assistant
message prefill') used to recursively re-create themselves every
retry by polluting history.
- BaseAgent.sanitizeForLlm filters status='error' / '[错误] ' prefix
assistant messages from history before LLM dispatch.
- BaseAgent.fetchHistoryMessages defensively drops trailing
AssistantMessages — Claude rejects assistant-tail prompts.
- NodeStreamingChatHelper.dropTrailingAssistant runs the same
defense at every doStreamCall pre-egress, so the in-turn
summarizing→reasoning transition (which leaves an assistant
scaffold at the tail) doesn't trip Claude either.
- AgentGraphBuilder.FallbackPolicy.DEEPSEEK switched (null,true,true)
→ (' ',false,true), aligning with KIMI/OPENAI's tolerant ' '
fallback. The previous 'force explicit 400' design was the
self-replicating loop's prime mover.
narration + tool args truncation:
- ReasoningNode.DEFAULT_MAX_OUTPUT_TOKENS 4096 → 16384. The 4k cap
was decapitating renderDocx tool_call args mid-stream when the
model emitted a long content field on top of thinking content;
the resulting 'invalid JSON' aborted execution silently.
- ReasoningNode appends a hermes-style TOOL_USE_ENFORCEMENT clause
to every system prompt: 'when you say you will perform an action,
call the tool now in the same response — narration is a protocol
violation'. Treats 'now I will generate the docx' (and never
actually calling renderDocx) as a forbidden pattern.
- ToolExecutionExecutor.normalizeToolExecutionError reframes the
JSON-truncated error as actionable instructions: 're-call the
same tool now with shorter content or split into multiple
sequential calls; do NOT describe the result as text'.
side fixes from the same evening:
- ChatController doOnComplete skips completionPublisher.publish
when isError=true, keeping memory extraction off the garbage path.
- ChatController doOnComplete queued-message guard simplified to
'cr.queuedInput() != null', matching the other 4 sites in the
controller. The previous 'isInterruptFollowup || !wasStopped'
guard silently dropped queued messages when the user did
Stop-then-Enqueue (wasStopped=true && interruptType=null), losing
the freshly-typed follow-up message.
- prompts/graph/summarize-system.txt now distinguishes 'single
task' (default; output one cohesive summary) from 'multiple
independent sub-tasks' (use the子任务 N format). Stops the
summarizer from inventing '子任务 1: PRO-027' decomposition for
unitary requests like 'write me a project proposal'.
This commit is contained in:
parent
187197e804
commit
fcdb3fc15e
@ -1589,7 +1589,18 @@ public class AgentGraphBuilder {
|
|||||||
* those into new 400s.
|
* those into new 400s.
|
||||||
*/
|
*/
|
||||||
private enum FallbackPolicy {
|
private enum FallbackPolicy {
|
||||||
DEEPSEEK(null, true, true),
|
// RFC-049 follow-up (2026-04-27): DEEPSEEK previously used (null, true, true)
|
||||||
|
// to "surface explicit provider error" when the producer-side relay had no
|
||||||
|
// captured reasoning_content. In practice this kept failing every multi-tool
|
||||||
|
// turn that crossed a summarizing boundary — the summarizer-produced
|
||||||
|
// assistant message has no reasoning_content by construction, the relay
|
||||||
|
// iterator has no entry for it, and DeepSeek returns 400 inside the same
|
||||||
|
// turn (not just multi-turn replay), aborting the whole graph at the
|
||||||
|
// reasoning step right after summarizing. Switching to the same " "
|
||||||
|
// tolerance KIMI/OPENAI use restores forward progress; the producer-side
|
||||||
|
// capture gap remains a real bug to fix in RFC-049 PR-3 but doesn't
|
||||||
|
// belong on the user-facing failure path.
|
||||||
|
DEEPSEEK(" ", false, true),
|
||||||
KIMI (" ", false, false),
|
KIMI (" ", false, false),
|
||||||
OPENAI (" ", false, false),
|
OPENAI (" ", false, false),
|
||||||
DEFAULT (" ", false, false);
|
DEFAULT (" ", false, false);
|
||||||
|
|||||||
@ -222,6 +222,20 @@ public abstract class BaseAgent {
|
|||||||
messages.add(springMessage);
|
messages.add(springMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tail guard: a few providers reject prompts whose history ends with an
|
||||||
|
// assistant message. Anthropic Claude returns 400 "does not support
|
||||||
|
// assistant message prefill"; DeepSeek thinking mode requires the
|
||||||
|
// last assistant turn's reasoning_content (which we may not have).
|
||||||
|
// The trailing-user-dedup above can already produce an assistant tail
|
||||||
|
// when the immediately-prior turn was an error / placeholder that got
|
||||||
|
// dropped by stage 1 / 1.5 of sanitizeForLlm. Strip remaining assistant
|
||||||
|
// tails defensively — the current user message is fed in separately as
|
||||||
|
// the final prompt by the caller, so dropping these assistant entries
|
||||||
|
// never loses information the LLM needs.
|
||||||
|
while (!messages.isEmpty() && messages.get(messages.size() - 1) instanceof AssistantMessage) {
|
||||||
|
messages.remove(messages.size() - 1);
|
||||||
|
}
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,6 +278,25 @@ public abstract class BaseAgent {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stage 1.5: drop typed-error assistant messages. These are persisted
|
||||||
|
// by ChatController.doOnComplete with status='error' (or carry the
|
||||||
|
// "[错误] " prefix injected by NodeStreamingChatHelper for legacy
|
||||||
|
// rows). Re-sending them as multi-turn context drives a self-replicating
|
||||||
|
// failure loop:
|
||||||
|
// - DeepSeek thinking mode → 400 "reasoning_content must be passed back"
|
||||||
|
// (we never captured a real reasoning_content for the failed turn)
|
||||||
|
// - Anthropic Claude → 400 "does not support assistant message prefill"
|
||||||
|
// (the trailing-user-dedup at the call site can leave an assistant
|
||||||
|
// tail when the prior turn errored)
|
||||||
|
// Both providers' 400 then re-persist a fresh "[错误] " row, repeat.
|
||||||
|
if ("assistant".equals(entity.getRole())
|
||||||
|
&& ("error".equals(entity.getStatus())
|
||||||
|
|| (entity.getContent() != null && entity.getContent().startsWith("[错误] ")))) {
|
||||||
|
log.debug("[{}] Filtering error assistant message from history: msgId={} status={}",
|
||||||
|
agentName, entity.getId(), entity.getStatus());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Delegate stages 2-4 to toSpringMessage; the stage 3 scrub is applied
|
// Delegate stages 2-4 to toSpringMessage; the stage 3 scrub is applied
|
||||||
// there so the rendered content is replaced before the typed Message
|
// there so the rendered content is replaced before the typed Message
|
||||||
// wrapper is constructed.
|
// wrapper is constructed.
|
||||||
|
|||||||
@ -575,6 +575,16 @@ public class NodeStreamingChatHelper {
|
|||||||
// The returned Prompt shares `options` by reference with the input prompt.
|
// The returned Prompt shares `options` by reference with the input prompt.
|
||||||
Prompt outbound = stripThinkingFromPrompt(prompt);
|
Prompt outbound = stripThinkingFromPrompt(prompt);
|
||||||
|
|
||||||
|
// RFC-049 follow-up (2026-04-27): trim trailing AssistantMessage from the
|
||||||
|
// outbound prompt. Triggered in practice by the summarizing→reasoning
|
||||||
|
// graph transition: the summarizer emits an in-turn AssistantMessage,
|
||||||
|
// graph state ends with it, reasoning's next LLM call sends history
|
||||||
|
// ending with assistant. Anthropic Claude returns 400 "does not
|
||||||
|
// support assistant message prefill"; some DeepSeek model variants 400
|
||||||
|
// similarly. The dropped assistant is summarizer scaffolding, not
|
||||||
|
// user-relevant content, so removing it before egress is safe.
|
||||||
|
outbound = dropTrailingAssistant(outbound);
|
||||||
|
|
||||||
// PR-2 L3 (RFC-049 §2.3.2): producer-side relay stash. Extract per-assistant
|
// PR-2 L3 (RFC-049 §2.3.2): producer-side relay stash. Extract per-assistant
|
||||||
// thinking from the normalized prompt (cross-turn positions are already "" due
|
// thinking from the normalized prompt (cross-turn positions are already "" due
|
||||||
// to strip), stash with the caller's original `user` field, and overwrite
|
// to strip), stash with the caller's original `user` field, and overwrite
|
||||||
@ -1064,6 +1074,37 @@ public class NodeStreamingChatHelper {
|
|||||||
return new Prompt(cleaned, prompt.getOptions());
|
return new Prompt(cleaned, prompt.getOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop trailing {@link AssistantMessage} entries from a Prompt's instructions.
|
||||||
|
* Most LLM providers reject prompts whose history ends with an assistant turn —
|
||||||
|
* Anthropic Claude with a 400 "does not support assistant message prefill",
|
||||||
|
* DeepSeek thinking-mode variants with reasoning_content errors. The
|
||||||
|
* trailing assistant is typically a summarizer-emitted scaffold message that
|
||||||
|
* shouldn't be sent as the final user-facing prompt anyway. Returns the
|
||||||
|
* input unchanged if there's nothing to drop.
|
||||||
|
*/
|
||||||
|
static Prompt dropTrailingAssistant(Prompt prompt) {
|
||||||
|
List<Message> messages = prompt.getInstructions();
|
||||||
|
if (messages.isEmpty()) {
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
int end = messages.size();
|
||||||
|
while (end > 0 && messages.get(end - 1) instanceof AssistantMessage) {
|
||||||
|
end--;
|
||||||
|
}
|
||||||
|
if (end == messages.size()) {
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
if (end == 0) {
|
||||||
|
// Refuse to produce an empty prompt — caller's bug; let provider error surface.
|
||||||
|
log.warn("[dropTrailingAssistant] all messages were AssistantMessage; skipping trim to avoid empty prompt");
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
log.debug("[dropTrailingAssistant] trimmed {} trailing AssistantMessage(s) from prompt (size {} -> {})",
|
||||||
|
messages.size() - end, messages.size(), end);
|
||||||
|
return new Prompt(new ArrayList<>(messages.subList(0, end)), prompt.getOptions());
|
||||||
|
}
|
||||||
|
|
||||||
private StreamResult buildErrorResult(String errorMsg, String conversationId, String phase) {
|
private StreamResult buildErrorResult(String errorMsg, String conversationId, String phase) {
|
||||||
log.error("[{}] Building error result for conversation {}: {}", phase, conversationId, errorMsg);
|
log.error("[{}] Building error result for conversation {}: {}", phase, conversationId, errorMsg);
|
||||||
if (streamTracker != null && conversationId != null) {
|
if (streamTracker != null && conversationId != null) {
|
||||||
|
|||||||
@ -739,8 +739,20 @@ public class ToolExecutionExecutor {
|
|||||||
|| lower.contains("unexpected character escape sequence")
|
|| lower.contains("unexpected character escape sequence")
|
||||||
|| lower.contains("json parse error")
|
|| lower.contains("json parse error")
|
||||||
|| lower.contains("malformed json")) {
|
|| lower.contains("malformed json")) {
|
||||||
return "Tool execution failed: model generated invalid JSON for tool arguments. "
|
// Truncated tool_call args — typically from max_tokens being hit while
|
||||||
+ "或在字符串转义位置被截断。请改为分步骤写入,拆成多个文件,或缩小单次 write_file/edit_file 的内容后重试。";
|
// streaming a large `content` field (e.g. renderDocx with 7000+ char
|
||||||
|
// markdown body). The fix MUST come from the model: re-emit the same
|
||||||
|
// tool call with smaller content per call, OR split the work across
|
||||||
|
// multiple sequential tool calls. We tell the LLM directly so the
|
||||||
|
// next reasoning iteration knows what to do — without this, models
|
||||||
|
// tend to fall back to narrating the result as final_answer text.
|
||||||
|
return "Tool execution failed: your tool_call arguments JSON was truncated mid-stream "
|
||||||
|
+ "(very likely you hit max_tokens while emitting a long content field). "
|
||||||
|
+ "Action required: re-call the SAME tool now in your next response, but "
|
||||||
|
+ "(1) make the content field shorter, OR (2) split the work into multiple "
|
||||||
|
+ "sequential tool calls (e.g. write the doc in 2-3 chunks via separate calls). "
|
||||||
|
+ "Do NOT describe the result as text — you must call the tool again to actually "
|
||||||
|
+ "produce the output.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lower.contains("access denied") && lower.contains("path outside allowed directories")) {
|
if (lower.contains("access denied") && lower.contains("path outside allowed directories")) {
|
||||||
|
|||||||
@ -52,8 +52,37 @@ public class ReasoningNode implements NodeAction {
|
|||||||
|
|
||||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
/** 单次 LLM 调用的默认最大输出 token 数,防止退化输出无限生成 */
|
/**
|
||||||
private static final int DEFAULT_MAX_OUTPUT_TOKENS = 4096;
|
* 单次 LLM 调用的默认最大输出 token 数,防止退化输出无限生成。
|
||||||
|
* <p>
|
||||||
|
* RFC-049 follow-up (2026-04-27): bumped 4096 → 16384. 4096 was hitting
|
||||||
|
* the cap when models emit large generative tool_call args (e.g. renderDocx
|
||||||
|
* with a multi-thousand-character markdown body) on top of thinking
|
||||||
|
* content for reasoning_effort=high — the JSON args got truncated mid-
|
||||||
|
* stream, the tool failed to parse, the docx was never generated. 16k is
|
||||||
|
* the conservative ceiling that covers typical "write a long document"
|
||||||
|
* tool calls without enabling true runaway loops (those are bounded by
|
||||||
|
* iteration count, not per-call tokens).
|
||||||
|
*/
|
||||||
|
private static final int DEFAULT_MAX_OUTPUT_TOKENS = 16384;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hermes-agent style enforcement clause appended to every ReasoningNode
|
||||||
|
* system prompt. Treats narration ("I will now …") as a protocol violation
|
||||||
|
* to prevent the recurring failure mode where a model says it will call a
|
||||||
|
* tool but emits the description as final_answer text instead.
|
||||||
|
*/
|
||||||
|
private static final String TOOL_USE_ENFORCEMENT = "\n\n"
|
||||||
|
+ "## 工具调用纪律(必读)\n\n"
|
||||||
|
+ "- 你**必须**直接调用工具来产生结果,不允许只用文字描述\"接下来要做什么\"。\n"
|
||||||
|
+ "- 当你说要执行某个动作(如生成文件、发送消息、调用接口、生成 docx),\n"
|
||||||
|
+ " 你**必须**在同一条回复里**立即发出对应的 tool_call**,不允许只写文字承诺。\n"
|
||||||
|
+ "- 禁止以\"现在 / 接下来 / 我将 / 直接生成 / 我直接\"+动作描述结束本轮回复——\n"
|
||||||
|
+ " 这种叙述会让系统误判任务已完成,**实际上工具没被调用**,结果文件不会产生。\n"
|
||||||
|
+ "- 如果上一次工具调用因 args JSON 截断(max_tokens 超限)失败,\n"
|
||||||
|
+ " 请重新调用同一工具但**缩小内容**,或拆成多次顺序调用,**不要改成纯文字回答**。\n"
|
||||||
|
+ "- 只在确实没有合适工具,或所有工具步骤都已完成、可以最终回答用户时,\n"
|
||||||
|
+ " 才输出无 tool_call 的纯文字回答。\n";
|
||||||
|
|
||||||
private final ChatModel chatModel;
|
private final ChatModel chatModel;
|
||||||
private final List<ToolCallback> toolCallbacks;
|
private final List<ToolCallback> toolCallbacks;
|
||||||
@ -203,6 +232,18 @@ public class ReasoningNode implements NodeAction {
|
|||||||
|
|
||||||
// ======= 构建 Prompt =======
|
// ======= 构建 Prompt =======
|
||||||
String systemPrompt = accessor.systemPrompt();
|
String systemPrompt = accessor.systemPrompt();
|
||||||
|
// RFC-049 follow-up: append a tool-use enforcement clause to every
|
||||||
|
// ReasoningNode call. Without this, models (especially DeepSeek thinking
|
||||||
|
// and Claude Opus) tend to "narrate" — emit a final_answer like "现在
|
||||||
|
// 直接生成立项材料 docx" instead of actually calling renderDocx, which
|
||||||
|
// makes the graph silently terminate at final_answer_node with the
|
||||||
|
// narration as the user-facing reply.
|
||||||
|
//
|
||||||
|
// Pattern adopted from hermes-agent's TOOL_USE_ENFORCEMENT_GUIDANCE
|
||||||
|
// (`/agent/prompt_builder.py:179-191`). Appended to systemPrompt rather
|
||||||
|
// than woven into the AgentEntity-stored prompt so it stays out of the
|
||||||
|
// user-editable agent UI but is still always-on at runtime.
|
||||||
|
systemPrompt = systemPrompt + TOOL_USE_ENFORCEMENT;
|
||||||
List<Message> messages = accessor.messages();
|
List<Message> messages = accessor.messages();
|
||||||
|
|
||||||
// Guard against runaway message list growth.
|
// Guard against runaway message list growth.
|
||||||
|
|||||||
@ -455,16 +455,28 @@ public class ChatController {
|
|||||||
})
|
})
|
||||||
.doOnComplete(() -> {
|
.doOnComplete(() -> {
|
||||||
if (!finalized.compareAndSet(false, true)) return;
|
if (!finalized.compareAndSet(false, true)) return;
|
||||||
// 区分三种完成语义:
|
// 区分四种完成语义:
|
||||||
// 1. 正常完成(stopRequested=false)→ completed
|
// 1. 正常完成(stopRequested=false)→ completed
|
||||||
// 2. 用户主动停止 → stopped
|
// 2. 用户主动停止 → stopped
|
||||||
// 3. 用户中断后续跑(interrupt-with-followup)→ interrupted
|
// 3. 用户中断后续跑(interrupt-with-followup)→ interrupted
|
||||||
|
// 4. LLM 客户端错误 / typed error → error
|
||||||
|
// (NodeStreamingChatHelper 把 typed error 序列化为 "[错误] "
|
||||||
|
// 前缀的文本作为 content_delta 注入,accumulator 不区分,
|
||||||
|
// 这里靠前缀识别。打标 status='error' 后,BaseAgent
|
||||||
|
// 的 history sanitization 阶段会跳过这类消息,避免下次
|
||||||
|
// prompt 被污染——DeepSeek thinking mode 缺
|
||||||
|
// reasoning_content 立即 400,Claude 不接受 assistant
|
||||||
|
// prefill 也 400,二者循环复制错误。)
|
||||||
boolean wasStopped = streamTracker.isStopRequested(conversationId);
|
boolean wasStopped = streamTracker.isStopRequested(conversationId);
|
||||||
ChatStreamTracker.InterruptType interruptType = streamTracker.getInterruptType(conversationId);
|
ChatStreamTracker.InterruptType interruptType = streamTracker.getInterruptType(conversationId);
|
||||||
boolean isInterruptFollowup = interruptType == ChatStreamTracker.InterruptType.USER_INTERRUPT_WITH_FOLLOWUP;
|
boolean isInterruptFollowup = interruptType == ChatStreamTracker.InterruptType.USER_INTERRUPT_WITH_FOLLOWUP;
|
||||||
|
boolean isError = accumulator.getContent() != null
|
||||||
|
&& accumulator.getContent().startsWith("[错误] ");
|
||||||
String persistStatus;
|
String persistStatus;
|
||||||
if (accumulator.isAwaitingApproval()) {
|
if (accumulator.isAwaitingApproval()) {
|
||||||
persistStatus = "awaiting_approval";
|
persistStatus = "awaiting_approval";
|
||||||
|
} else if (isError) {
|
||||||
|
persistStatus = "error";
|
||||||
} else if (!wasStopped) {
|
} else if (!wasStopped) {
|
||||||
persistStatus = "completed";
|
persistStatus = "completed";
|
||||||
} else {
|
} else {
|
||||||
@ -488,8 +500,11 @@ public class ChatController {
|
|||||||
savedAssistant = conversationService.saveMessage(conversationId, "assistant",
|
savedAssistant = conversationService.saveMessage(conversationId, "assistant",
|
||||||
isInterruptFollowup ? "[已中断]" : "[已停止生成]", null, persistStatus);
|
isInterruptFollowup ? "[已中断]" : "[已停止生成]", null, persistStatus);
|
||||||
}
|
}
|
||||||
// 发布对话完成事件(仅正常完成时,停止/中断不触发记忆提取)
|
// 发布对话完成事件(仅正常完成时;停止/中断/错误均不触发记忆提取)
|
||||||
if (!wasStopped) {
|
// RFC-049 follow-up: also skip on isError — error turns persist
|
||||||
|
// garbage like "[错误] Bad request..." as the assistant reply,
|
||||||
|
// which would pollute the memory extraction pipeline if propagated.
|
||||||
|
if (!wasStopped && !isError) {
|
||||||
completionPublisher.publish(agentId, conversationId, message, assistantText, "web");
|
completionPublisher.publish(agentId, conversationId, message, assistantText, "web");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -520,7 +535,19 @@ public class ChatController {
|
|||||||
streamTracker.clearInterruptState(conversationId);
|
streamTracker.clearInterruptState(conversationId);
|
||||||
ChatStreamTracker.CompletionResult cr = streamTracker.completeAndConsumeIfLast(conversationId);
|
ChatStreamTracker.CompletionResult cr = streamTracker.completeAndConsumeIfLast(conversationId);
|
||||||
if (cr.allDone()) {
|
if (cr.allDone()) {
|
||||||
if (cr.queuedInput() != null && (isInterruptFollowup || !wasStopped)) {
|
// RFC follow-up (2026-04-27): the previous guard
|
||||||
|
// cr.queuedInput() != null && (isInterruptFollowup || !wasStopped)
|
||||||
|
// dropped legitimate queued messages when the user stopped
|
||||||
|
// the running turn and then sent a new message via the
|
||||||
|
// enqueue path (not the interrupt-with-followup path) —
|
||||||
|
// wasStopped=true + isInterruptFollowup=false made the
|
||||||
|
// guard false, the consumed queuedInput was discarded,
|
||||||
|
// and the user's new message vanished. The other 4 sites
|
||||||
|
// in this controller already use the simpler "if queued,
|
||||||
|
// run it" condition; align with them. If the user
|
||||||
|
// genuinely doesn't want continuation, no message would
|
||||||
|
// have been in messageQueue to begin with.
|
||||||
|
if (cr.queuedInput() != null) {
|
||||||
startQueuedMessage(conversationId, emitter, emitterDone, cr.queuedInput(), username);
|
startQueuedMessage(conversationId, emitter, emitterDone, cr.queuedInput(), username);
|
||||||
} else {
|
} else {
|
||||||
conversationService.updateStreamStatus(conversationId, "idle");
|
conversationService.updateStreamStatus(conversationId, "idle");
|
||||||
|
|||||||
@ -1,14 +1,32 @@
|
|||||||
你是一个信息整理助手。请基于用户的原始问题和多轮工具调用的观察结果,生成一份结构化的上下文摘要。
|
你是一个信息整理助手。请基于用户的原始问题和多轮工具调用的观察结果,生成一份结构化的上下文摘要。
|
||||||
|
|
||||||
要求:
|
## 任务结构判断(先做这一步)
|
||||||
1. 识别用户问题中的每个独立子任务或子问题
|
|
||||||
2. 每个子任务的结果必须保留为独立段落,使用编号或标题区分,不要合并不同子任务的结果
|
判断用户的原始问题包含的是单一任务还是多个独立子任务:
|
||||||
3. 每个子任务段落中保留关键结论、具体数据和支撑证据
|
|
||||||
4. 删除每个子任务内部的重复或冗余内容,但不要跨子任务合并
|
- **单一任务**:例如"帮我写一份立项材料"、"生成一份周报"、"重构这个文件" — 用户表达的是一个完整目标,即使涉及读多个文件、多步操作,也仍然是一个目标。这种情况下**不要**强行拆分子任务。
|
||||||
5. 对信息不确定的地方明确标注
|
- **多个独立子任务**:例如"先调研 A 再调研 B 然后对比"、"分别给我 X 的信息、Y 的信息、Z 的信息" — 用户的问题里有 2 个及以上明显独立、可以独立完成的子任务。
|
||||||
6. 不要包含原始工具调用日志或技术细节
|
- 当不确定时,倾向于按"单一任务"处理。
|
||||||
7. 每个子任务的摘要控制在 400 字以内,总输出不超过 2000 字
|
|
||||||
8. 使用清晰的结构化格式:先列出子任务标题,再给出该子任务的要点列表
|
## 输出要求
|
||||||
|
|
||||||
|
### 单一任务(默认情况)
|
||||||
|
|
||||||
|
- 输出**一份连贯的摘要**,结构清晰即可,不要套用"子任务 1 / 子任务 2"格式。
|
||||||
|
- 保留关键结论、关键数据、关键决策依据。
|
||||||
|
- 删除重复或冗余内容。
|
||||||
|
- 对信息不确定的地方明确标注。
|
||||||
|
- 不要包含原始工具调用日志或技术细节。
|
||||||
|
- 总输出不超过 2000 字。
|
||||||
|
|
||||||
|
### 多个独立子任务
|
||||||
|
|
||||||
|
- 每个子任务的结果必须保留为独立段落,使用编号或标题区分,不要合并不同子任务的结果。
|
||||||
|
- 每个子任务段落中保留关键结论、具体数据和支撑证据。
|
||||||
|
- 删除每个子任务内部的重复或冗余内容,但不要跨子任务合并。
|
||||||
|
- 对信息不确定的地方明确标注。
|
||||||
|
- 每个子任务的摘要控制在 400 字以内,总输出不超过 2000 字。
|
||||||
|
- 使用清晰的结构化格式:先列出子任务标题,再给出该子任务的要点列表。
|
||||||
|
|
||||||
格式示例:
|
格式示例:
|
||||||
### 子任务 1: [任务描述]
|
### 子任务 1: [任务描述]
|
||||||
@ -17,4 +35,4 @@
|
|||||||
|
|
||||||
### 子任务 2: [任务描述]
|
### 子任务 2: [任务描述]
|
||||||
- 结论要点 1
|
- 结论要点 1
|
||||||
- 结论要点 2
|
- 结论要点 2
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user