Tutorials / LangChain Integration
Add a Text Humanization Tool to Your LangChain Agent
A step-by-step guide to building a LangChain custom tool that calls the ToHuman API — from a standalone tool definition to a full agent that generates, humanizes, and publishes content in a single pipeline.
LangChain has become the default framework for building LLM-powered agents — 97,000 GitHub stars and a standard spot in almost every serious Python AI stack. If you're using LangChain agents to generate content, summarize documents, or run multi-step writing pipelines, you've probably run into a predictable problem: the text your agent produces reads like it came from a machine, because it did.
AI detectors like GPTZero and Turnitin flag this output not because the content is bad, but because the patterns are statistically recognizable. The fix is text humanization via the ToHuman API — a post-processing step that rewrites AI-generated text to pass detection without altering the meaning. This tutorial shows you how to wire that step directly into your LangChain agent as a custom tool, so the agent can humanize its own output as part of its reasoning loop.
Three things are covered: building the tool with the @tool decorator, adding it to both the legacy AgentExecutor and the newer create_react_agent pattern, and building a complete generate-humanize-publish pipeline. There's also a section on when to use the tool inside the agent loop versus running humanization as a deterministic post-processing step.
Why LangChain Agents Need a Humanization Tool
LLM agents generate text at every step — drafting answers, writing intermediate summaries, producing final outputs. All of that text carries the statistical fingerprint of the model that wrote it. When that output is the end product (a blog post, an email, a report), it will score as AI-generated on any standard detection tool.
The common workaround is to run the output through a humanizer manually after the agent finishes. That breaks automation. If your agent is the thing doing the work, you want humanization to be another step in the chain — something the agent can call when it decides the output needs it, or something you trigger automatically before returning the final result to the user.
LangChain's tool system is exactly the right primitive for this. Tools are Python functions the agent can call during its reasoning loop. The agent reads each tool's name and docstring to decide when to use it. Give the humanizer a clear description and the agent will reach for it at the right moment.
Prerequisites
- Python 3.9+ with
langchain,langchain-openai, andhttpxinstalled. - An OpenAI API key stored as
OPENAI_API_KEYin your environment (or swap in any LangChain-supported LLM). - A ToHuman API key — sign up free at tohuman.io. Store it as
TOHUMAN_API_KEY.
Install the dependencies:
Terminal
pip install langchain langchain-openai httpx
Step 1: Create the Humanization Tool
LangChain's @tool decorator turns any Python function into a tool an agent can call. The function's docstring is the tool's description — the LLM reads this to decide when and how to use it. Write the docstring like you're explaining the tool to a smart colleague who doesn't know your codebase.
Create a file called tools.py:
tools.py
import os
import httpx
from langchain_core.tools import tool
TOHUMAN_API_KEY = os.environ["TOHUMAN_API_KEY"]
TOHUMAN_API_URL = "https://tohuman.io/api/v1/humanize"
@tool
def humanize_text(text: str, strength: str = "medium") -> str:
"""
Rewrites AI-generated text to sound naturally human-written.
Use this tool when you have generated text that needs to pass AI
detection tools or simply read more naturally. The tool sends the
text to the ToHuman API and returns a rewritten version that
preserves the original meaning while removing AI writing patterns.
Args:
text: The AI-generated text to humanize. Works best on
complete paragraphs or full sections rather than
individual sentences.
strength: How aggressively to rewrite. One of:
- "light" — minor word-level adjustments
- "medium" — sentence-level rewrites (default)
- "strong" — structural rewrites, most thorough
Returns:
The humanized version of the input text as a string.
"""
response = httpx.post(
TOHUMAN_API_URL,
headers={
"Authorization": f"Bearer {TOHUMAN_API_KEY}",
"Content-Type": "application/json",
},
json={"text": text, "strength": strength},
timeout=60,
)
response.raise_for_status()
return response.json()["humanized_text"]
A few things worth noting here. The timeout=60 is important — humanization of longer pieces can take several seconds, and the default httpx timeout (5 seconds) will cut it off. The raise_for_status() call turns 4xx and 5xx responses into exceptions that LangChain's agent loop can catch and handle. And the docstring is doing real work: it tells the agent what the strength parameter means so it can choose the right value when calling the tool.
Test the tool directly before wiring it into an agent:
Python REPL — verify the tool works standalone
from tools import humanize_text
result = humanize_text.invoke({
"text": "Artificial intelligence has demonstrated remarkable capabilities "
"across numerous domains, enabling the automation of complex tasks "
"that previously required significant human expertise.",
"strength": "medium"
})
print(result)
You should get back a naturally rewritten version of the input. If you see a 401 error, check that TOHUMAN_API_KEY is set correctly in your environment. A 422 means the request body is invalid — the most common cause is passing strength a value outside the accepted set (light, medium, strong).
Step 2: Add the Tool to a LangChain Agent
There are two agent patterns in current LangChain use. The newer create_react_agent approach (from LangGraph) is preferred for new projects. AgentExecutor is still common in existing codebases. Both take a tools list — the wiring is the same.
Option A — create_react_agent (recommended)
This is the LangGraph-based approach introduced in LangChain v0.1. It compiles to a graph that supports streaming and checkpointing:
agent.py — create_react_agent pattern
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
from tools import humanize_text
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
agent = create_react_agent(
model=llm,
tools=[humanize_text],
prompt=(
"You are a content writer. When you produce final written content, "
"always humanize it using the humanize_text tool before returning it. "
"Set the strength to 'medium' unless the user asks for something different."
),
)
result = agent.invoke({
"messages": [
("user", "Write a 150-word introduction for a blog post about the "
"rise of AI coding assistants in 2026.")
]
})
# The final message is the agent's last response
print(result["messages"][-1].content)
The system prompt is doing the key work here: it instructs the agent to always call humanize_text before returning written content. Without this instruction, the agent will only call the tool when it independently decides to — which may be less often than you want for a content pipeline. Be explicit in the prompt about when the tool should be used.
Option B — AgentExecutor (legacy)
If you're adding humanization to an existing agent built with AgentExecutor, the integration is one line — add humanize_text to the tools list you pass at construction:
agent_legacy.py — AgentExecutor pattern
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate
from tools import humanize_text
# Add humanize_text alongside your existing tools
tools = [humanize_text] # ... plus your other tools
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
prompt = ChatPromptTemplate.from_messages([
("system",
"You are a content writer. Always humanize final written content "
"with the humanize_text tool before returning it to the user."),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
result = executor.invoke({
"input": "Write a 150-word intro about AI coding assistants in 2026."
})
print(result["output"])
Set verbose=True during development — it prints each tool call and its arguments, so you can confirm the agent is calling humanize_text and with what strength value. Turn it off in production.
Step 3: Run the Agent and Verify Output
Run your agent script and watch the intermediate steps. With verbose mode enabled, you'll see something like this in the terminal:
Terminal output — agent reasoning trace
> Entering new AgentExecutor chain...
Invoking: `humanize_text` with {'text': 'Artificial intelligence coding assistants
have emerged as transformative tools in the software development landscape...', 'strength': 'medium'}
The developer landscape has shifted. AI coding assistants went from novelty to
necessity in the span of a single product cycle...
> Finished chain.
The trace shows the tool being called, the exact arguments passed, and the humanized output returned. If the tool call doesn't appear in the trace, the agent decided it wasn't needed based on the prompt. Adjust the system prompt to be more explicit: "You must call humanize_text on any written content before returning it."
To verify the humanized output actually clears AI detectors, run it through GPTZero's API or paste it into the web interface. For a more systematic setup, the AI detection false positives guide covers what detectors look for and how humanization addresses each signal.
Advanced: Build a Generate-Humanize-Publish Pipeline
For production content workflows, you often want humanization to happen automatically without relying on the agent's own judgment. The pattern here is to run it as a deterministic post-processing step — the agent generates, and you call the ToHuman API directly on whatever it returns before passing the output downstream.
This approach works well when you're building a pipeline that chains content generation into a publishing step — for example, a script that generates a batch of blog drafts and posts each one to your CMS after humanizing it:
pipeline.py — generate, humanize, and publish in sequence
import os
import httpx
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
TOHUMAN_API_KEY = os.environ["TOHUMAN_API_KEY"]
TOHUMAN_API_URL = "https://tohuman.io/api/v1/humanize"
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.4)
def generate_draft(topic: str) -> str:
"""Call the LLM directly to produce a draft."""
response = llm.invoke([
HumanMessage(content=(
f"Write a 300-word blog section about: {topic}. "
"Use flowing paragraphs — no headers or bullet points."
))
])
return response.content
def humanize(text: str, strength: str = "medium") -> str:
"""Post-process any text through the ToHuman API."""
response = httpx.post(
TOHUMAN_API_URL,
headers={
"Authorization": f"Bearer {TOHUMAN_API_KEY}",
"Content-Type": "application/json",
},
json={"text": text, "strength": strength},
timeout=60,
)
response.raise_for_status()
return response.json()["humanized_text"]
def publish_to_cms(title: str, content: str) -> None:
"""Replace this with your actual CMS API call."""
print(f"[CMS] Publishing: {title}")
print(content[:200] + "...")
def run_pipeline(topics: list[str]) -> None:
for topic in topics:
print(f"\nProcessing: {topic}")
draft = generate_draft(topic)
humanized = humanize(draft, strength="medium")
publish_to_cms(title=topic, content=humanized)
if __name__ == "__main__":
run_pipeline([
"How AI pair programming changes solo developer workflows",
"The case for keeping humans in the AI review loop",
"Why code review is getting harder as AI output grows",
])
This pipeline is simpler than an agent loop for high-volume batch work. There's no LLM decision-making involved in whether to humanize — it always happens. For the CMS publish step, swap in your actual API call: WordPress REST API, Webflow, Contentful, Ghost — all of them accept POST requests to create content programmatically.
Handling errors in production
In a batch pipeline, a single failed humanization request shouldn't kill the whole run. Wrap the humanize call in a try/except and decide what to do on failure — skip the item, retry once, or log it for manual reprocessing:
pipeline.py — error handling for batch runs
import logging
logger = logging.getLogger(__name__)
def humanize_with_fallback(text: str, strength: str = "medium") -> str:
"""
Attempt humanization; return original text on failure
so the pipeline can continue running.
"""
try:
return humanize(text, strength)
except httpx.HTTPStatusError as e:
logger.error("ToHuman API error %s for text starting: %.80s",
e.response.status_code, text)
return text # fall back to un-humanized draft
except httpx.TimeoutException:
logger.error("ToHuman API timeout — returning original draft")
return text
The fallback returns the original draft so the pipeline can publish something rather than failing silently. You can revisit the flagged items later. For the strength parameter in automated pipelines, "medium" is the right default — it handles raw LLM output well without the occasional meaning-drift that "strong" can introduce on content with precise factual claims.
When to Use the Tool vs Post-Processing
Use the tool inside the agent loop when you want the agent to decide when humanization is appropriate — for example, a general-purpose research and writing agent where only some outputs are intended for publication. The agent reads the tool description and decides when to call it based on context.
Use deterministic post-processing when humanization should always happen — content pipelines, batch generators, and publishing workflows where the output always goes to a channel where AI detection is a concern. This is more reliable than trusting the agent's judgment, and it's easier to test.
The two approaches aren't mutually exclusive. For a complex agent with multiple output types, you can define the tool for agent use and also call the API directly in your application layer before passing any written output to end users.
What You've Built
You have a LangChain tool that calls the ToHuman API, integrated into both the modern create_react_agent pattern and the legacy AgentExecutor. You've also got a standalone pipeline that generates, humanizes, and publishes content without involving the agent's reasoning loop — which is the right pattern for high-volume batch work.
From here, you can extend the tool to support the full strength range, add retry logic for transient API errors, or chain multiple tools together — for example, a research tool that finds sources, a writing tool that drafts, and the humanizer that processes the final output. The ToHuman API docs cover all available parameters and response fields.
If you're working in a no-code or low-code environment instead, the Make.com humanization tutorial and the n8n integration guide cover the same generate-humanize-publish pattern without any Python required.
Frequently Asked Questions
How do I create a custom tool in LangChain that calls an external API?
Use the @tool decorator from langchain_core.tools on a Python function. The function's docstring becomes the tool's description — the LLM reads this to decide when to call it. Make your API request inside the function using httpx or requests, and return a string. LangChain handles passing the return value back into the agent's reasoning loop automatically.
What's the difference between AgentExecutor and create_react_agent?
create_react_agent is the newer LangGraph-based approach. It compiles to a graph that supports streaming, interruption, and checkpointing. AgentExecutor is the legacy approach — simpler to set up but less flexible. For new projects, use create_react_agent. The tool wiring is identical in both: pass your tools list at construction time.
Will the agent always call the humanize tool?
Only if the prompt tells it to. Include an explicit instruction in the system prompt: "Always humanize your final written content before returning it." If you want guaranteed humanization regardless of agent behavior, run it as a post-processing step in your application layer after agent.invoke() returns — call the ToHuman API directly on the output string.
What strength value should I use from a LangChain agent?
For raw LLM output with no prior editing, use "medium" or "strong". Use "light" only for content that has already been through some human review. Stronger settings produce more natural prose but can occasionally rephrase factual claims — always review before publishing anything that includes precise statistics or quotes.
Published April 7, 2026 by the ToHuman team.