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

前四篇我们从零搭建了ReAct智能体,完成了Pydantic结构化输出、工具Schema自动生成、原生Function Calling协议接入、四态状态机、LLM-as-Judge回答完整性校验等核心能力。但跑复杂场景时暴露了三个问题:
- 单工具串行瓶颈 :代码硬编码
resp.tool_calls[0],只处理第一个工具调用,OpenAI原生FC支持一次返回多个tool_calls(比如同时读两个文件、同时统计行数+读取文件),我们白白浪费了并行能力; - Agent只能读不能做:只有read_file+calculator两个只读工具,Agent能"看"和"算"但不能"执行"------不能跑命令、不能看目录结构、不能运行脚本测试,本质上还是个只读顾问;
- Judge是"盲审":上一版Judge只看到用户问题和AI回答,看不到中间调用了什么工具、工具返回了什么结果,无法判断"AI是不是真的读过文件再回答的"还是"心算瞎编的"。
本文完成三大升级:
- 多工具并行支持:遍历tool_calls数组批量执行,每个工具结果带各自tool_call_id存入memory,对齐OpenAI多工具协议;
- BashTool(Shell执行工具):带工作目录沙箱、危险命令黑名单、超时控制、输出截断、退出码/stderr展示,Agent从"只读顾问"升级为"能读能写能执行的Coding Agent";
- Judge证据链增强:引入tool_trace工具调用轨迹,Judge能看到完整的"问题→工具调用→工具结果→回答"链路,判断依据从文本覆盖升级为基于证据的事实校验。
前置说明
- 改造文件:
tools/bash.py(新增BashTool)、main.py(多工具并行+Judge证据链+Prompt修正); - 零改动文件:
llm_client.py(ToolCall模型和解析逻辑天然支持tool_calls数组,不需要改)、agent/memory.py(add_tool_result(tool_call_id, content)天然支持多条tool结果分别配对)、tools/base_tool.py、tools/calculator.py、tools/file_reader.py------架构可扩展性验证通过; - 删除: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 /、sudo、mkfs、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 | 加新工具=建类+注册,零侵入验证通过 |
八、后续拓展
- 真正的并发执行 :用
concurrent.futures.ThreadPoolExecutor并行执行独立工具调用,减少多工具场景的总等待时间; - FileWriteTool/FileEditTool:写文件/修改文件能力(需备份+确认机制),Agent从"分析"升级为"改代码";
- Plan-then-Execute模式:复杂任务先分解为子任务计划,执行中跟踪进度,所有子任务完成才FINISH;
- 命令白名单:当前黑名单模式无法穷举所有危险命令,生产环境需要白名单模式(只允许特定命令前缀);
- Hook/Callback机制:抽离AgentEngine类,支持before_llm/after_tool/on_judge等事件钩子,方便加日志、trace、指标;
- 长期记忆/RAG:Chroma向量库接入,解决上下文窗口限制。
九、Java/Go后端快速语法映射
list[ToolCall]遍历 = JavaList<ToolCall>for-each / Go[]*ToolCallfor rangesubprocess.run(shell=True, capture_output=True, timeout=N)= JavaProcessBuilder+waitFor(timeout)/ Goexec.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 #多工具并行