【Java/Go后端手撸原生Agent(第五篇):多工具并行调用 + BashTool执行引擎 + Judge证据链升级】

"我":Java/Go后端开发者、有点时间想自己琢磨,想入门Agent但不想堆砌框架、希望理解底层原理的研发

上一篇链接:Java/Go后端手撸原生Agent(第四篇)

前言

前四篇我们从零搭建了ReAct智能体,完成了Pydantic结构化输出、工具Schema自动生成、原生Function Calling协议接入、四态状态机、LLM-as-Judge回答完整性校验等核心能力。但跑复杂场景时暴露了三个问题:

  1. 单工具串行瓶颈 :代码硬编码resp.tool_calls[0],只处理第一个工具调用,OpenAI原生FC支持一次返回多个tool_calls(比如同时读两个文件、同时统计行数+读取文件),我们白白浪费了并行能力;
  2. Agent只能读不能做:只有read_file+calculator两个只读工具,Agent能"看"和"算"但不能"执行"------不能跑命令、不能看目录结构、不能运行脚本测试,本质上还是个只读顾问;
  3. Judge是"盲审":上一版Judge只看到用户问题和AI回答,看不到中间调用了什么工具、工具返回了什么结果,无法判断"AI是不是真的读过文件再回答的"还是"心算瞎编的"。

本文完成三大升级:

  1. 多工具并行支持:遍历tool_calls数组批量执行,每个工具结果带各自tool_call_id存入memory,对齐OpenAI多工具协议;
  2. BashTool(Shell执行工具):带工作目录沙箱、危险命令黑名单、超时控制、输出截断、退出码/stderr展示,Agent从"只读顾问"升级为"能读能写能执行的Coding Agent";
  3. Judge证据链增强:引入tool_trace工具调用轨迹,Judge能看到完整的"问题→工具调用→工具结果→回答"链路,判断依据从文本覆盖升级为基于证据的事实校验。

前置说明

  1. 改造文件:tools/bash.py(新增BashTool)、main.py(多工具并行+Judge证据链+Prompt修正);
  2. 零改动文件:llm_client.py(ToolCall模型和解析逻辑天然支持tool_calls数组,不需要改)、agent/memory.pyadd_tool_result(tool_call_id, content)天然支持多条tool结果分别配对)、tools/base_tool.pytools/calculator.pytools/file_reader.py------架构可扩展性验证通过;
  3. 删除:System Prompt中手写的工具能力描述段(违反SSOT)。

一、问题复现与根因分析

1.1 痛点1:硬编码0浪费多工具能力

上一版TOOL_EXECUTING状态:

python 复制代码
pending_tool_call = resp.tool_calls[0]  # 只取第一个
# ...执行一个工具...
memory.add_tool_result(pending_tool_call.id, obs)

OpenAI API支持parallel_tool_calls,模型可以一次返回多个tool_calls(例如同时调用read_file读两个文件、或同时调wc统计行数+read_file读内容)。只取0意味着:

  • 模型调用了3个工具,但我们只执行1个,剩下2个永远没有对应的tool消息回传,下一轮LLM收到的上下文是不完整的;
  • 多轮才能拿到的信息被迫拆成多轮,浪费LLM调用次数。

根因:第一版原生FC接入时为了简化实现只取了第一个,属于"最小可行版本"的临时方案,协议本身完全支持多工具。

1.2 痛点2:没有执行能力,Agent是残废的

只有read_file+calculator意味着Agent只能"看"和"算"。当用户说"帮我看看项目结构"、"跑一下main.py看报什么错"、"用grep搜索哪里定义了xxx"时,Agent无能为力------它没有执行shell命令的能力。

根因:缺少执行类工具。BashTool是Coding Agent最核心的工具------有了它Agent才能:

  • ls/find探索目录结构
  • cat/grep搜索和查看文件(比read_file更灵活)
  • python3 xxx.py运行脚本看结果
  • wc/head/tail做快速文本统计
1.3 痛点3:Judge是"盲审",看不到证据链

上一版Judge输入:

python 复制代码
f"【用户问题】{user_query}\n\n【AI回答】{answer}"

Judge只能做"文本覆盖检查"------回答里有没有提到数字、有没有提到方法名。但它无法判断:

  • AI说"execute方法在第28行",但AI到底有没有调用read_file?
  • AI说"计算结果是30",但它调了calculator吗?还是心算的?
  • 工具返回了错误信息,AI有没有如实报告?

根因:Judge缺少"证据"------工具调用轨迹。司法审判需要证据链,Agent回答质量判断同样需要:AI调用了什么工具、参数是什么、工具返回了什么,这些都是判断回答质量的事实基础。


二、改造1:多工具并行调用

2.1 设计思路

OpenAI多工具协议的消息结构:

复制代码
assistant消息: {
  "role": "assistant",
  "content": "我需要同时读取文件和计算",
  "tool_calls": [
    {"id": "call_1", "type": "function", "function": {"name": "read_file", "arguments": "{}"}},
    {"id": "call_2", "type": "function", "function": {"name": "calculator", "arguments": "{}"}}
  ]
}
tool消息1: {"role": "tool", "tool_call_id": "call_1", "content": "文件内容..."}
tool消息2: {"role": "tool", "tool_call_id": "call_2", "content": "30"}

关键点:

  • assistant消息一次性携带完整的tool_calls数组;
  • 每个tool_call有独立id;
  • 多个tool消息紧跟在assistant消息后面,顺序和tool_calls数组顺序一致(OpenAI协议要求);
  • 每个tool消息通过tool_call_id精确配对到对应的调用。

我们当前的架构天然支持------add_assistant已经接收tool_calls数组参数,add_tool_result接收tool_call_id参数。只需要把TOOL_EXECUTING从"处理单个ToolCall"改为"遍历listToolCall"即可。

2.2 核心代码改动

main.py中pending变量从单个对象改为list:

python 复制代码
# 改动前
pending_tool_call: Optional[ToolCall] = None

# 改动后
pending_tool_calls: list[ToolCall] = []

THINKING状态保存全部调用:

python 复制代码
pending_tool_calls = list(resp.tool_calls)  # 保存全部,不再取[0]

TOOL_EXECUTING状态改为for循环遍历:

python 复制代码
if state == AgentTaskState.TOOL_EXECUTING:
    for tc in pending_tool_calls:
        call_key = (tc.name, json.dumps(tc.arguments, sort_keys=True, ensure_ascii=False))

        if call_key in executed_calls:
            obs = f"[系统提示] 你已经用完全相同的参数调用过{tc.name}..."
            print(f"【重复调用拦截】{tc.name} {tc.arguments}")
        else:
            print(f"【工具调用】{tc.name}({json.dumps(tc.arguments, ensure_ascii=False)})")
            tool = tool_map.get(tc.name)
            if not tool:
                obs = f"错误:不存在工具{tc.name}"
            else:
                obs = tool.execute(tc.arguments)
            display_obs = obs[:200] + ("..." if len(obs) > 200 else "")
            print(f"【工具返回】{display_obs}")
            executed_calls.add(call_key)
            tool_trace.append({"name": tc.name, "args": tc.arguments, "result": obs})

        # 每个工具都要存一条tool_result,用各自的id配对
        memory.add_tool_result(tc.id, obs)

    pending_tool_calls = []
    state = AgentTaskState.THINKING
    continue

关于"并行"的说明:这里用for循环顺序执行,但从LLM视角看是并行的------因为assistant消息里同时声明了所有tool_calls,下一轮THINKING时所有tool结果一次性出现在messages里。真正的并发执行(用concurrent.futures.ThreadPoolExecutor)在学习阶段没必要,会增加调试复杂度,但结果对LLM是一样的。


三、新增BashTool(Shell执行引擎)

3.1 安全设计的四层防御

BashTool是最强大的工具也是最危险的工具------rm -rf /一行命令就能毁灭一切。安全设计必须分层防御,不能靠一层就想挡住所有攻击:

防御层 机制 阻挡什么
第1层:危险命令黑名单 关键词匹配直接拦截 rm -rf /sudomkfs、fork bomb、`curl
第2层:工作目录沙箱 cwd.relative_to(WORKSPACE_ROOT)校验 访问工作区外目录(/tmp/etc/passwd等)
第3层:超时控制 subprocess.run(timeout=N) 命令hang住(死循环、sleep 999、网络阻塞等)
第4层:输出截断 >3000字符截断加标记 撑爆context window(find /输出几万行)

额外细节:

  • 退出码可见:命令成功返回0+stdout,失败返回非零退出码+stderr,LLM能看到错误原因;
  • curl/wget单独判断:下载文件本身不危险,管道给sh/bash执行才危险;
  • shell=True:允许管道、重定向、通配符等shell特性,学习阶段LLM自己生成命令不是外部用户输入,安全可控。
3.2 完整tools/bash.py代码
python 复制代码
"""
Bash工具:执行shell命令。
"""
from __future__ import annotations

import subprocess
import platform
from pathlib import Path
from typing import Optional

from pydantic import BaseModel, Field

from tools.base_tool import BaseTool


WORKSPACE_ROOT = Path(__file__).resolve().parent.parent

# 危险命令黑名单:匹配到就直接拦截
DANGEROUS_PATTERNS = [
    "rm -rf /", "rm -rf /*", "rm -rf ~", "rm -rf $HOME",
    "sudo ", "su ", "mkfs", "dd if=",
    ":(){:|:&};:",  # fork bomb
    "chmod -R 777 /", "chown -R ",
    "reboot", "shutdown", "halt", "poweroff", "init 0",
    "> /dev/sd",  # 格式化磁盘
    "kill -9 1",  # 杀init进程
]

OUTPUT_MAX_CHARS = 3000
DEFAULT_TIMEOUT = 10
MAX_TIMEOUT = 30


class BashArgs(BaseModel):
    command: str = Field(description="要执行的shell命令,例如 'ls -la'、'python3 main.py'、'grep -r import .'")
    timeout: int = Field(default=DEFAULT_TIMEOUT, ge=1, le=MAX_TIMEOUT,
                         description=f"超时秒数,默认{DEFAULT_TIMEOUT},最大{MAX_TIMEOUT}")
    cwd: Optional[str] = Field(default=None, description="执行目录绝对路径,必须在工作区内,默认工作区根目录")


class BashTool(BaseTool):

    @property
    def name(self) -> str:
        return "bash"

    @property
    def desc(self) -> str:
        return f"执行shell命令(bash/sh),支持ls/cat/grep/find/python3/pip/git等," \
               f"运行在macOS/{platform.system()}环境"

    @property
    def args_schema(self) -> type[BaseModel]:
        return BashArgs

    def run(self, args: BashArgs) -> str:
        # ---- 第1层:危险命令检查 ----
        cmd_lower = args.command.lower()
        for pattern in DANGEROUS_PATTERNS:
            if pattern.lower() in cmd_lower:
                if pattern in ("curl ", "wget "):
                    if "| sh" not in cmd_lower and "| bash" not in cmd_lower:
                        continue
                return f"[安全拦截] 命令包含危险模式 '{pattern.strip()}',已阻止执行。"

        # ---- 第2层:工作目录沙箱 ----
        if args.cwd:
            cwd_path = Path(args.cwd).resolve()
            try:
                cwd_path.relative_to(WORKSPACE_ROOT)
                work_dir = str(cwd_path)
            except ValueError:
                return f"[安全拦截] 执行目录 {args.cwd} 不在工作区内,禁止访问工作区外目录。"
        else:
            work_dir = str(WORKSPACE_ROOT)

        # ---- 第3层:执行命令(超时控制)----
        try:
            result = subprocess.run(
                args.command,
                shell=True,
                cwd=work_dir,
                capture_output=True,
                text=True,
                timeout=args.timeout,
            )
        except subprocess.TimeoutExpired:
            return f"[命令超时] 命令在{args.timeout}秒内未完成执行,已终止。"
        except Exception as e:
            return f"[执行异常] {type(e).__name__}: {e}"

        # ---- 第4层:结果组装+输出截断 ----
        stdout = result.stdout or ""
        stderr = result.stderr or ""
        exit_code = result.returncode

        parts = [f"[退出码] {exit_code}", f"[执行目录] {work_dir}"]
        if stdout:
            parts.append(f"[stdout]\n{stdout}")
        if stderr:
            parts.append(f"[stderr]\n{stderr}")
        if not stdout and not stderr:
            parts.append("[输出] 命令无输出")

        output = "\n".join(parts)
        if len(output) > OUTPUT_MAX_CHARS:
            truncated = output[:OUTPUT_MAX_CHARS]
            output = truncated + f"\n\n[输出截断] 原始输出{len(output)}字符,已截断为{OUTPUT_MAX_CHARS}字符。"

        return output
3.3 注册工具

main.py中加入BashTool注册:

python 复制代码
from tools.bash import BashTool

tool_list: list[BaseTool] = [CalcTool(), FileReadTool(), BashTool()]
tool_map = {t.name: t for t in tool_list}

加新工具不需要改Prompt、不需要改Parser、不需要改主循环------这是Phase 2工具Schema自动生成的价值兑现。


四、改造3:Judge证据链增强

4.1 设计思路

让Judge看到完整证据链:

复制代码
【用户问题】...
【AI调用过的工具及结果摘要】
1. 工具: read_file, 参数: {"path": "tools/base_tool.py"}
   返回: [文件: ...] 共56行...
2. 工具: bash, 参数: {"command": "wc -l tools/base_tool.py"}
   返回: [退出码] 0\n[stdout]\n56 tools/base_tool.py
3. 工具: calculator, 参数: {"expr": "(6+9)*4/2"}
   返回: 计算结果: (6+9)*4/2 = 30.0
【AI回答】...

Judge基于这些信息可以判断:

  • AI确实调了read_file,回答里对方法的解释是有依据的;
  • AI确实调了bash做wc统计;
  • AI确实调了calculator,回答里的30是工具算出来的不是心算;
  • 如果工具返回了错误(比如文件不存在),AI有没有如实报告。
4.2 引入tool_trace记录轨迹

run_agent中新增trace变量:

python 复制代码
tool_trace: list[dict] = []  # 记录每次实际工具调用的name/args/result

工具实际执行成功(非重复拦截)后追加记录:

python 复制代码
# 在TOOL_EXECUTING的else分支(实际执行工具后)
tool_trace.append({"name": tc.name, "args": tc.arguments, "result": obs})

注意:重复调用拦截不追加trace(不算新调用)。

4.3 _format_tool_trace辅助函数

把trace格式化为简洁文本,长参数/长结果截断,控制Judge的token消耗:

python 复制代码
def _format_tool_trace(tool_trace: list[dict]) -> str:
    if not tool_trace:
        return "(AI未调用任何工具)"
    lines = []
    for i, t in enumerate(tool_trace, 1):
        args_str = json.dumps(t["args"], ensure_ascii=False)
        if len(args_str) > 200:
            args_str = args_str[:200] + "..."
        result_summary = t["result"]
        if len(result_summary) > 300:
            result_summary = result_summary[:300] + "..."
        lines.append(f"{i}. 工具: {t['name']}, 参数: {args_str}\n   返回: {result_summary}")
    return "\n".join(lines)
4.4 Judge升级

JUDGE_SYSTEM_PROMPT增加证据判断标准:

python 复制代码
JUDGE_SYSTEM_PROMPT = """你是回答质量检查员。你的任务是判断"AI助手的回答"是否完整覆盖了"用户的问题"中的所有要求。

你会收到三部分信息:
1. 【用户问题】:用户的原始提问
2. 【AI调用过的工具及结果摘要】:AI在回答过程中调用了哪些工具、参数是什么、工具返回了什么
3. 【AI回答】:AI最终给出的回答

判断标准:
- 用户问了几个问题/任务点,回答里每个都要有对应内容;
- 如果用户要求数值计算且AI调用了calculator,回答必须引用计算器返回的具体数值;
- 如果用户要求读取文件/代码且AI调用了read_file,回答必须基于文件实际内容,不能编造;
- 如果用户要求执行命令且AI调用了bash,回答必须包含命令执行结果;
- 如果AI根本没有调用必要的工具(例如要求计算但没调calculator),判定为不通过;
- 如果工具调用失败了(返回错误),AI必须在回答中说明错误,不能假装成功;
- 回答允许有推理过程,但必须最终给出完整答案。

输出严格JSON格式:
{"passed": true/false, "missing": "具体缺失内容描述,passed为true时为空字符串"}

注意:
- missing字段要具体指出缺什么(例如"AI没有调用calculator工具计算数值"),不要泛泛说"回答不完整";
- 如果回答覆盖了所有问题点且正确使用了工具结果,passed=true,missing为空。
"""

judge_answer函数签名增加tool_trace参数:

python 复制代码
def judge_answer(user_query: str, answer: str, tool_trace: list[dict]) -> JudgeResult:
    if not answer or len(answer.strip()) < 10:
        return JudgeResult(passed=False, missing="回答过短,没有实质内容。")

    trace_text = _format_tool_trace(tool_trace)
    messages = [
        {"role": "system", "content": JUDGE_SYSTEM_PROMPT},
        {"role": "user", "content": (
            f"【用户问题】{user_query}\n\n"
            f"【AI调用过的工具及结果摘要】\n{trace_text}\n\n"
            f"【AI回答】{answer}\n\n"
            f"请判断AI回答是否完整覆盖了用户的所有问题点,且正确使用了工具结果。"
        )},
    ]
    resp = chat_completion(messages, json_mode=True)
    # ...解析逻辑不变...

VALIDATING状态调用时传入tool_trace:

python 复制代码
judge_res = judge_answer(user_query, pending_draft_answer, tool_trace)

五、main.py完整代码

python 复制代码
"""
Phase 7:多工具并行调用 + BashTool(Shell执行能力) + Judge证据链增强
"""
from __future__ import annotations

import json
from enum import Enum
from typing import Optional

from pydantic import BaseModel, Field

from agent.memory import ShortMemory
from llm_client import ToolCall, chat_completion
from tools.base_tool import BaseTool
from tools.bash import BashTool
from tools.calculator import CalcTool
from tools.file_reader import FileReadTool


class AgentTaskState(Enum):
    THINKING = "thinking"
    TOOL_EXECUTING = "tool_executing"
    VALIDATING = "validating"
    FINISHED = "finished"


tool_list: list[BaseTool] = [CalcTool(), FileReadTool(), BashTool()]
tool_map = {t.name: t for t in tool_list}


SYSTEM_PROMPT = """你是一个智能代码助手,可以使用提供的工具来帮助用户完成任务。

规则:
1. 需要获取外部信息时(读取文件、计算数值、执行命令等),调用相应工具;
2. 可以一次调用多个工具并行获取信息;
3. 仔细阅读工具返回结果(包括退出码和stderr),基于结果进行推理;
4. 信息足够回答用户问题时,直接用清晰的自然语言回答,不要调用不必要的工具;
5. 如果用户要求数值计算,必须使用calculator工具,不要心算;
6. 如果工具返回错误信息,分析错误原因,可以换参数重试或告知用户;
7. 执行命令前确认安全,不要执行rm -rf、sudo等毁灭性命令;
8. 回答要结构清晰、条理分明、完整覆盖用户的所有问题点,使用中文。
"""


class JudgeResult(BaseModel):
    passed: bool = Field(description="回答是否完整覆盖了用户的所有问题点")
    missing: str = Field(description="不通过时具体缺少什么;通过时为空字符串")


JUDGE_SYSTEM_PROMPT = """你是回答质量检查员。你的任务是判断"AI助手的回答"是否完整覆盖了"用户的问题"中的所有要求。

你会收到三部分信息:
1. 【用户问题】:用户的原始提问
2. 【AI调用过的工具及结果摘要】:AI在回答过程中调用了哪些工具、参数是什么、工具返回了什么
3. 【AI回答】:AI最终给出的回答

判断标准:
- 用户问了几个问题/任务点,回答里每个都要有对应内容;
- 如果用户要求数值计算且AI调用了calculator,回答必须引用计算器返回的具体数值;
- 如果用户要求读取文件/代码且AI调用了read_file,回答必须基于文件实际内容,不能编造;
- 如果用户要求执行命令且AI调用了bash,回答必须包含命令执行结果;
- 如果AI根本没有调用必要的工具,判定为不通过;
- 如果工具调用失败了,AI必须在回答中说明错误,不能假装成功;
- 回答允许有推理过程,但必须最终给出完整答案。

输出严格JSON格式:
{"passed": true/false, "missing": "具体缺失内容描述,passed为true时为空字符串"}
"""


def _format_tool_trace(tool_trace: list[dict]) -> str:
    if not tool_trace:
        return "(AI未调用任何工具)"
    lines = []
    for i, t in enumerate(tool_trace, 1):
        args_str = json.dumps(t["args"], ensure_ascii=False)
        if len(args_str) > 200:
            args_str = args_str[:200] + "..."
        result_summary = t["result"]
        if len(result_summary) > 300:
            result_summary = result_summary[:300] + "..."
        lines.append(f"{i}. 工具: {t['name']}, 参数: {args_str}\n   返回: {result_summary}")
    return "\n".join(lines)


def judge_answer(user_query: str, answer: str, tool_trace: list[dict]) -> JudgeResult:
    if not answer or len(answer.strip()) < 10:
        return JudgeResult(passed=False, missing="回答过短,没有实质内容。")

    trace_text = _format_tool_trace(tool_trace)
    messages = [
        {"role": "system", "content": JUDGE_SYSTEM_PROMPT},
        {"role": "user", "content": (
            f"【用户问题】{user_query}\n\n"
            f"【AI调用过的工具及结果摘要】\n{trace_text}\n\n"
            f"【AI回答】{answer}\n\n"
            f"请判断AI回答是否完整覆盖了用户的所有问题点,且正确使用了工具结果。"
        )},
    ]
    resp = chat_completion(messages, json_mode=True)
    if not resp.content:
        return JudgeResult(passed=True, missing="")
    try:
        data = json.loads(resp.content)
        return JudgeResult(**data)
    except Exception:
        return JudgeResult(passed=True, missing="")


def run_agent(user_query: str):
    memory = ShortMemory()
    memory.add_user(user_query)
    max_loop = 15

    state = AgentTaskState.THINKING
    loop_count = 0
    judge_retry_count = 0
    max_judge_retries = 3
    final_answer = None
    pending_tool_calls: list[ToolCall] = []
    pending_draft_answer: Optional[str] = None
    executed_calls: set[tuple[str, str]] = set()
    tool_trace: list[dict] = []

    tools_schema = [t.to_openai_tool_schema() for t in tool_list]

    while state != AgentTaskState.FINISHED and loop_count < max_loop:
        loop_count += 1

        if state == AgentTaskState.THINKING:
            messages = [{"role": "system", "content": SYSTEM_PROMPT}]
            messages.extend(memory.get_messages())

            print(f"\n=== 第{loop_count}轮 THINKING ===")
            resp = chat_completion(messages, tools=tools_schema)

            if resp.has_tool_calls:
                tool_calls_dicts = [tc.to_openai_dict() for tc in resp.tool_calls]
                memory.add_assistant(content=resp.content, tool_calls=tool_calls_dicts)

                if resp.content:
                    thought = resp.content[:200] + ("..." if len(resp.content) > 200 else "")
                    print(f"【推理思考】{thought}")

                pending_tool_calls = list(resp.tool_calls)
                state = AgentTaskState.TOOL_EXECUTING
                continue
            else:
                pending_draft_answer = resp.content
                state = AgentTaskState.VALIDATING
                continue

        if state == AgentTaskState.TOOL_EXECUTING:
            for tc in pending_tool_calls:
                call_key = (tc.name, json.dumps(tc.arguments, sort_keys=True, ensure_ascii=False))

                if call_key in executed_calls:
                    obs = f"[系统提示] 你已经用完全相同的参数调用过{tc.name}..."
                    print(f"【重复调用拦截】{tc.name} {tc.arguments}")
                else:
                    print(f"【工具调用】{tc.name}({json.dumps(tc.arguments, ensure_ascii=False)})")
                    tool = tool_map.get(tc.name)
                    obs = f"错误:不存在工具{tc.name}" if not tool else tool.execute(tc.arguments)
                    print(f"【工具返回】{obs[:200]}{'...' if len(obs) > 200 else ''}")
                    executed_calls.add(call_key)
                    tool_trace.append({"name": tc.name, "args": tc.arguments, "result": obs})

                memory.add_tool_result(tc.id, obs)

            pending_tool_calls = []
            state = AgentTaskState.THINKING
            continue

        if state == AgentTaskState.VALIDATING:
            print("【校验】正在Judge回答完整性...")
            judge_res = judge_answer(user_query, pending_draft_answer, tool_trace)

            if judge_res.passed or judge_retry_count >= max_judge_retries:
                if judge_retry_count >= max_judge_retries:
                    print(f"【警告】Judge已连续{max_judge_retries}次打回,强制通过")
                else:
                    print("【校验通过】回答完整")
                memory.add_assistant(content=pending_draft_answer)
                final_answer = pending_draft_answer
                state = AgentTaskState.FINISHED
                break
            else:
                judge_retry_count += 1
                print(f"【门禁拦截】缺失: {judge_res.missing}")
                correction = (
                    f"你的回答不完整,缺少以下内容:{judge_res.missing}\n"
                    f"请基于已有工具结果补充回答,不要重复调用已调用的工具。"
                )
                memory.add_user(f"[Judge反馈] {correction}")
                pending_draft_answer = None
                state = AgentTaskState.THINKING
                continue

    if final_answer is None:
        final_answer = f"达到最大循环次数{max_loop},任务未完成"
    return final_answer


if __name__ == "__main__":
    answer = run_agent(
        "帮我分析 tools/base_tool.py 文件:"
        "1.用bash的wc -l命令统计文件总行数;"
        "2.用read_file读取execute和to_openai_tool_schema方法的内容并解释它们做了什么;"
        "3.这两个方法的行数加起来乘以4再除以2等于多少(必须用calculator计算)。"
    )
    print("\n最终回答:", answer)

六、BashTool测试验证

注:windows下的命令或有差异

测试场景 结果
echo hello world 退出码0,正确输出hello world
wc -l tools/base_tool.py 退出码0,正确统计56行
rm -rf / 安全拦截 阻止执行
ls nonexistent_file 退出码1,stderr正确显示"No such file"
ls cwd=/tmp 安全拦截 工作目录沙箱阻止访问工作区外目录
sleep 100 timeout=2 2秒后命令超时终止

七、核心改造总结

维度 Phase 6(改造前) Phase 7(改造后)
工具调用数量 硬编码0,单工具串行 遍历tool_calls数组,多工具并行(LLM视角)
工具能力 read_file + calculator(只读+计算) +bash(读+算+执行),最小可用Coding Agent
Bash安全 四层防御:黑名单/沙箱/超时/截断
Judge输入 用户问题 + AI回答(盲审) +工具调用轨迹(证据链审判)
Judge判断标准 文本覆盖检查 证据使用校验+工具调用完整性+错误透明性
System Prompt 包含工具能力列表(回退) 只含行为规则,工具信息走tools参数(SSOT修复)
架构扩展性 加新工具需改Prompt 加新工具=建类+注册,零侵入验证通过

八、后续拓展

  1. 真正的并发执行 :用concurrent.futures.ThreadPoolExecutor并行执行独立工具调用,减少多工具场景的总等待时间;
  2. FileWriteTool/FileEditTool:写文件/修改文件能力(需备份+确认机制),Agent从"分析"升级为"改代码";
  3. Plan-then-Execute模式:复杂任务先分解为子任务计划,执行中跟踪进度,所有子任务完成才FINISH;
  4. 命令白名单:当前黑名单模式无法穷举所有危险命令,生产环境需要白名单模式(只允许特定命令前缀);
  5. Hook/Callback机制:抽离AgentEngine类,支持before_llm/after_tool/on_judge等事件钩子,方便加日志、trace、指标;
  6. 长期记忆/RAG:Chroma向量库接入,解决上下文窗口限制。

九、Java/Go后端快速语法映射

  • list[ToolCall]遍历 = Java List<ToolCall> for-each / Go []*ToolCall for range
  • subprocess.run(shell=True, capture_output=True, timeout=N) = Java ProcessBuilder + waitFor(timeout) / Go exec.CommandContext
  • 四层防御 = Filter链 / Middleware链模式
  • tool_trace证据链 = 分布式链路追踪(traceId记录完整调用链)
  • Judge基于证据判断 = 代码Review基于diff+测试结果而非仅看最终输出
  • 黑名单拦截 = WAF规则 / SQL注入过滤
  • fail-open策略(Judge异常时放行)= 熔断器降级模式
  • tool_call_id配对 = 消息队列correlationId / RPC requestId

下一篇链接:【Java/Go后端手撸原生Agent(第六篇):流式输出------让Agent思考过程"看得见"】

标签:#java #golang #后端 #Agent #python #Pydantic #状态机 #Function-Calling #Bash工具 #LLM-as-Judge #多工具并行

相关推荐
奈何不吃鱼1 小时前
【SpringBoot】业务多线程四大实战场景
java·spring boot·多线程
FfHUCisI1 小时前
Go语言程序结构 —— 变量、声明与零值机制
golang
野生风长1 小时前
c++类和对象(this指针,重载operator,习题总结)
java·开发语言·c++
霸道流氓气质1 小时前
基于 Spring 事务同步机制的事务后置动作收集器 Starter 实践
java·后端·spring
IT_陈寒2 小时前
Java线程池踩了个坑,任务居然默默消失了
前端·人工智能·后端
过期动态2 小时前
【LeetCode 热题 100】找到字符串中所有字母异位词
java·数据结构·算法·leetcode·职场和发展·rabbitmq
b130538100492 小时前
HarmonyOS开发实战:小分享-ForEach循环渲染与key生成策略
后端
程序员爱钓鱼2 小时前
配置 GoLand 与 VS Code 开发环境
前端·后端·go
程序员爱钓鱼2 小时前
Rust Vec 动态数组详解:创建、增删、遍历与排序
前端·后端·rust