























Hello @yech
Great question, and your instincts are mostly right. Let me break this down clearly for you.
When you were using the raw OpenAI SDK, you were assembling context manually, you controlled what went into the message list. With DeepAgents (which sits on top of LangGraph/LangChain), that control moves into tools. The agent calls your tools, and whatever you return from a tool becomes part of the context automatically. So instead of thinking “how do I arrange the context?”, think “how do I design my tools to return exactly the right data?”.
You’ve correctly identified that you need two separate tools. Here’s how to think about them:
This takes a free-form query and returns narrative information. No strict field validation needed.
from langchain_core.tools import tool
@tool
def get_company_overview(query: str) -> str:
"""Return general background information about Huawei Technologies Co., Ltd.
Use this when the user asks broad, open-ended questions about the company
(history, products, size, culture, etc.).
Args:
query: The user's free-form question about the company.
"""
# Call your data source / RAG pipeline / API here
return fetch_narrative_info(query)
This is where your design question lives. The parameter is a list[str] and the tool itself handles validation before doing anything expensive.
from langchain_core.tools import tool
from pydantic import BaseModel, field_validator
# These are your canonical field names — the source of truth
KNOWN_FIELDS: dict[str, str] = {
"website": "Official website URL",
"headquarters": "Headquarters location",
"founded": "Year founded",
"ceo": "Current CEO name",
"employees": "Approximate number of employees",
"revenue": "Latest annual revenue",
"stock_ticker": "Stock exchange and ticker symbol",
"phone": "Main contact phone number",
}
class CompanyFieldsInput(BaseModel):
fields: list[str]
@field_validator("fields")
@classmethod
def normalize_fields(cls, raw_fields: list[str]) -> list[str]:
resolved = []
for f in raw_fields:
match = resolve_field(f) # your semantic matching function
if match:
resolved.append(match)
return resolved
@tool(args_schema=CompanyFieldsInput)
def get_company_fields(fields: list[str]) -> dict[str, str]:
"""Fetch specific, structured data fields for Huawei Technologies Co., Ltd.
Use this when the user asks for one or more specific pieces of data
(e.g., website, CEO, revenue). Returns only the fields that exist.
Args:
fields: List of field names the user wants (e.g., ["website", "ceo"]).
"""
results = {}
for field in fields:
results[field] = fetch_field_from_source(field)
return results
This is the part where you prevent the model from hallucinating field names and passing garbage into your data layer. Here’s a clean pattern using LangChain embeddings:
from langchain_openai import OpenAIEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_core.documents import Document
# Build a small in-memory vector store of your known fields at startup
_embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
_field_store = InMemoryVectorStore.from_documents(
documents=[
Document(page_content=f"{name}: {desc}", metadata={"field": name})
for name, desc in KNOWN_FIELDS.items()
],
embedding=_embeddings,
)
def resolve_field(user_field: str, threshold: float = 0.75) -> str | None:
"""Map a user-supplied field name to a canonical field name.
Returns the canonical name if a confident match is found, else None.
Args:
user_field: Raw field name as provided by the user or LLM.
threshold: Minimum cosine similarity score to accept a match.
"""
results = _field_store.similarity_search_with_score(user_field, k=1)
if not results:
return None
doc, score = results[0]
if score >= threshold:
return doc.metadata["field"]
return None
If you don’t want to use embeddings (e.g., for lower latency), a lightweight alternative is rapidfuzz:
from rapidfuzz import process, fuzz
def resolve_field(user_field: str, threshold: int = 70) -> str | None:
match, score, _ = process.extractOne(
user_field,
KNOWN_FIELDS.keys(),
scorer=fuzz.WRatio
)
return match if score >= threshold else None
from deepagents import create_deep_agent
agent = create_deep_agent(
model="openai:gpt-4o",
tools=[get_company_overview, get_company_fields],
system_prompt=(
"You are a Huawei Technologies research assistant. "
"For broad questions use get_company_overview. "
"For specific data points (website, CEO, revenue, etc.) use get_company_fields. "
"You may call both in the same response if the user wants both."
),
)
result = agent.invoke({
"messages": [{
"role": "user",
"content": "Tell me about Huawei, and also give me their website and CEO."
}]
})
The agent will naturally decide to call both tools when the user asks for both general info and specific fields, because that’s exactly what the tool descriptions guide it to do.
You mentioned you used to arrange results yourself with the raw OpenAI SDK. In DeepAgents, you get equivalent control through two mechanisms:
Tool return values — Whatever your tool returns (string, dict, list) is serialized into a ToolMessage and injected into the conversation context. You control the shape of that data entirely inside the tool function.
response_format for structured final output — If you need the agent’s final answer to follow a strict schema (not just intermediate tool results), use the response_format parameter:
from pydantic import BaseModel
from langchain.agents.structured_output import ResponseFormat
class HuaweiReport(BaseModel):
summary: str
fields: dict[str, str]
missing_fields: list[str]
agent = create_deep_agent(
model="openai:gpt-4o",
tools=[get_company_overview, get_company_fields],
response_format=ResponseFormat(schema=HuaweiReport),
)
| Concern | Solution in DeepAgents |
|---|---|
| Two separate interfaces | Two @tool functions with clear descriptions |
| List of fields as input | args_schema=CompanyFieldsInput with Pydantic |
| Validate fields before querying | resolve_field() inside the tool (embeddings or fuzzy match) |
| Control what goes into context | Control what the tool returns |
| Structured final output | response_format=ResponseFormat(schema=YourModel) |
| User asks for both | Agent calls both tools naturally based on tool descriptions |
Your original thinking was sound, the key realization is that the tool function is the right place to put your validation and normalization logic, not in a separate pre-processing step outside the agent. This keeps the agent’s interface clean while protecting your data layer from bad inputs.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。